Linux Kernel

Linux I2C Slave Driver

We will discuss about the Linux implementation of the I2C slave device drivers. We will investigate the example driver and also browse through the implementation of the driver to understand the I2C client/slave drivers in detail.

Description:

I2C is the protocol of communication between two ICs. One IC refers to the master and the other IC refers to the slave. Let us take an example of the board if a new slave device is connected on the board on an already existing I2C BUS. The I2C bus is controlled by the I2C master. Connecting a new device requires the introduction of the I2C client driver in the Linux OS. Though, without the I2C slave driver, the device can be accessed with the i2c-utils through the I2C master but those access are purely I2C data/messages.

To understand it in more details, let us take an example of the eeprom device which is an I2C slave device. Once the eeprom is connected on the board, if the user queries through, the “i2cdetect” logs show the slave device on the bus with the eeprom slave address. But if this lower level I2C communication needs to be transparent to the user, the user can introduce the I2C slave driver for eeprom. The user need not to access the eeprom with the raw I2C commands through the i2c-utils. The user can access the eeprom through the device file and to the simple read/write to manipulate the data.

The Linux framework for the I2C slave driver expects the object for the “i2c_driver” struct with the proper values as the following example:

static struct i2c_driver eeprom_driver = {
    .driver = {
        .name   = "eeprom",
        .of_match_table = eeprom_of_match,
    },
    .probe      = eeprom_probe,
    .id_table   = eeprom_id,
};

 

.driver: This field takes the name of the driver and compatible string that are required for probe().

.probe: This is the probe function for the driver.

.id_table; This is the device ID for the device which is passed to the macro “MODULE_DEVICE_TABLE”.

Once this structure is populated, the driver can be registered through the following call:

module_i2c_driver(eeprom_driver);

 

The device ID creation can be done as follows:

static const struct i2c_device_id eeprom_id[] = {
    { "eeprom", eeprom },
    { }
};
MODULE_DEVICE_TABLE(i2c, eeprom_id);

 

Next, the important piece is the probe function for the driver. When the driver binds with the device, it calls the probe function. The probe function does all the setup of the resources and prepares the device for the functioning. The example of tasks which are done in the probe function are as follows:

  1. Allocating the memory resources
  2. Registering of interrupts if the device supports IRQ
  3. Device’s hardware programming…. etc.

The probe function is called only once when the device and the driver binding is happening. Once the binding of the device and driver is done, the device is ready for further read/write functions.

The standard prototype for the I2C slave driver in the Linux I2C framework is as follows:

static int eeprom_probe(struct i2c_client *client)

{.

.

.

}

Let us elaborate more on the probe function by considering the eeprom device. In the probe function, the driver can expose the device file and can register the read and write functions through “ioctl” or “sysfs”.

If the user reads or writes to the device file, this driver processes those requests through the read and write functions of the driver. We will discuss the read and write function of the driver further. For now, let us focus on the probe function. Setting up of this infrastructure creation of the device file and registering those read/write function with the device file is done in the probe function. Along with these, the device supports the interrupts. Then, the IRQ line request should be done and the registration of interrupt for this device to the IRQ table should also be done in the probe function. This is completely device dependent. For eeprom device, the general interrupts are not required.

Now, let us take the read/write functions for the I2C slave driver. There is no standard prototype that is defined for this but the lower level that is called to access the device should be done through the I2C function calls of the adapter since this is the slave device and it is associated with the I2C bus master. The I2C bus master is used to access the device. The example function for the I2C master communication is i2c_transfer() though there are many functions that exist. To use the i2c_transfer() function, one should create the “i2c_msg” object and pass the reference to the object to the i2c_transfer().

In our case, the example read and write functions can be as follows:

static ssize_t eeprom_write(struct eeprom_data *eeprom,
        const char *data_buf,
        unsigned data_offset,
        size_t data_count)
{
    struct i2c_client *client;
    struct i2c_data_msg data_msg;
    ssize_t status;
    unsigned long timeout, write_time;
    unsigned next_page;

    client = eeprom_translate_data_offset(eeprom, &data_offset);

    /* write_max is at most a page */
    if (data_count > eeprom->write_max)
        data_count = eeprom->write_max;

    next_page = roundup(data_offset + 1, eeprom->chip.page_size);
    if (data_offset + data_count > next_page)
        data_count = next_page - data_offset;

    if (!eeprom->use_smbus) {
        int i = 0;

        data_msg.addr = client->addr;
        data_msg.flags = 0;

        data_msg.data_buf = eeprom->writedata_buf;
        if (eeprom->chip.flags & eeprom_FLAG_ADDR16)
            data_msg.data_buf[i++] = data_offset >> 8;

        data_msg.data_buf[i++] = data_offset;
        memcpy(&data_msg.data_buf[i], data_buf, data_count);
        data_msg.len = i + data_count;
    }

    timeout = jiffies + msecs_to_jiffies(write_timeout);
    do {
        write_time = jiffies;
        if (eeprom->use_smbus) {
            status = i2c_smbus_write_i2c_block_data(client,
                    data_offset, data_count, data_buf);
            if (status == 0)
                status = data_count;
        } else {
            status = i2c_transfer(client->adapter, &data_msg, 1);
            if (status == 1)
                status = data_count;
        }
        if (status == data_count)
            return data_count;
        msleep(1);
    } while (time_before(write_time, timeout));

    return -ETIMEDOUT;
}

 

static ssize_t eeprom_read(struct eeprom_data *eeprom,
        char *data_buf,
        unsigned data_offset,
        size_t data_count)
{
    struct i2c_msg data_msg[2];
    u8 data_msgbuf[2];
    struct i2c_client *client;
    unsigned long timeout, read_time;
    int status, i;

    memset(msg, 0, sizeof(data_msg));

   client = eeprom_translate_offset(eeprom, &data_offset);

    if (data_count > io_limit)
        data_count = io_limit;

    switch (eeprom->use_smbus) {
    case I2C_SMBUS_I2C_BLOCK_DATA:
        if (data_count > I2C_SMBUS_BLOCK_MAX)
            data_count = I2C_SMBUS_BLOCK_MAX;
        break;
    case I2C_SMBUS_WORD_DATA:
        data_count = 2;
        break;
    case I2C_SMBUS_BYTE_DATA:
        data_count = 1;
        break;
    default:
        i = 0;
        if (eeprom->chip.flags & eeprom_FLAG_ADDR16)
            data_msgbuf[i++] = data_offset >> 8;
        data_msgbuf[i++] = data_offset;

        data_msg[0].addr = client->addr;
        data_msg[0].buf = msgbuf;
        data_msg[0].len = i;

        data_msg[1].addr = client->addr;
        data_msg[1].flags = I2C_M_RD;
        data_msg[1].buf = buf;
        data_msg[1].len = count;
    }
    timeout = jiffies + msecs_to_jiffies(write_timeout);
    do {
        read_time = jiffies;
        switch (eeprom->use_smbus) {
        case I2C_SMBUS_I2C_BLOCK_DATA:
            status = i2c_smbus_read_i2c_block_data(client, data_offset,
                    data_count, data_buf);
            break;
        case I2C_SMBUS_WORD_DATA:
            status = i2c_smbus_read_word_data(client, data_offset);
            if (status >= 0) {
                buf[0] = status & 0xff;
                buf[1] = status >> 8;
                status = data_count;
            }
            break;
        case I2C_SMBUS_BYTE_DATA:
            status = i2c_smbus_read_byte_data(client, data_offset);
            if (status >= 0) {
                buf[0] = status;
                status = data_count;
            }
            break;
        default:
            status = i2c_transfer(client->adapter, data_msg, 2);
            if (status == 2)
                status = data_count;
        }
        if (status == data_count)
            return data_count;

        msleep(1);
    } while (time_before(read_time, timeout));

    return -ETIMEDOUT;
}

 

These are the minimum functions required for the I2C slave driver in Linux. The complexity of the driver depends on the device’s capability and offerings.

Conclusion

So far, we discussed about the I2C slave driver framework for Linux. We took an example of eeprom device and tried to cover all the areas that we should be aware of for implementing the I2C slave driver. With this, our understanding for identifying the I2C slave driver should be good. We should be able to identify the I2C client device and interpret their drivers. We browsed through the driver framework with the help of eeprom but the other I2C slave devices like rtc, sensors, etc., should also employ the similar implementation and registration to the I2C subsystem of Linux.

About the author

Sushil Rathore

Sushil Rathore is having hands-on experience in Linux Platform SW. He's an expert of Linux on ARM/X86 Boards. He has very good understanding on Bootloaders and other platform softwares. He has good industrial experience and have worked in reputed Organizations. Currently he is associated with a reputed firm in the networking domain.