Writing an I2C Kernel Device Driver

·

4 min read

In this post I am going to write Linux device driver for Beaglebone Black to interface with Nunchuk Joystick and MCP9808 Temperature sensor using I2C protocol.

  • Part 1. Writing Kernel Driver for MCP9808 Temperature Sensor.
  • Part 2. Writing Kernel Driver for Nunchuk Joystick and using Input Subsystem

I2C Primer

I2C is a slow two-wire protocol which was developed by Philips. This protocol provides a bus for connecting multiple types of devices with infrequent or low bandwidth communication needs.

In Linux terminology, there are two chips: adapter chip and client chip. We need a driver for I2C adapter and another driver for I2C client/ device.

I2C Bus core I2C Controller Drivers I2C Device Drivers/ Client Drivers.

Implementation

Device Registration in Device Tree

For this project, I am connecting MCP9808 sensor to the I2C-1 bus. The device tree structure looks like this.

&i2c1 {
    pinctrl-names = "default";
    pinctrl-0 = <&i2c1_pins>;
    status = "okay";
    clock-frequency = <100000>;

    temp: mcp@18 {
        compatible = "microchip,mcp9808";
        reg = <0x18>;
    };
};

Driver Structure

The first building block of the I2C driver for the sensor looks like this.

// SPDX-License-Identifier: GPL-2.0
#include <linux/init.h>
#include <linux/module.h>
#include <linux/i2c.h>


static const struct of_device_id mcp9808_of_match[] = {
    { .compatible = "microchip,mcp9808" },
    { },
};

MODULE_DEVICE_TABLE(of, mcp9808_of_match);

static struct i2c_driver mcp9808_i2c_driver = {
    .driver = {
        .name = "mcp9808_i2c",
        .owner = THIS_MODULE,
        .of_match_table = mcp9808_of_match,
    },
    .probe = mcp9808_i2c_probe,
    .remove = mcp9808_i2c_remove,
};

// A macro to define default init() and exit() functions.
module_i2c_driver(mcp9808_i2c_driver);

MODULE_AUTHOR("John Doe <john.doe@gmail.com");
MODULE_DESCRIPTION("Basic I2C Temperature driver");
MODULE_LICENSE("GPL");

This structure is typically used in other kinds of device drivers. Here we define the name of the driver, its compatibility with the device tree structure, the probing and the remove function called during initialisation and unregistration of the device.

probe()

We can specify the steps to initialise the device in this function. In order to initialise and register MCP9808 device, we are going to execute these steps:

  1. Create a device node each device.
  2. Initialise some registers.
  3. Setup device-specific data.
static int mcp9808_i2c_probe(struct i2c_client *client,
                              const struct i2c_device_id *id)
{   
    pr_info("mcp9808_i2c_probe is called.\n");

    int error;
    u8 inp[2];
    error = mcp9808_read_registers(client, 0x06, inp, sizeof(inp));
    if (error == 0)
    {
        pr_info("MCP9808 Manufacture ID = %x\n", inp[0] << 8 | inp[1]);
    }

    struct mcp9808_dev *mcp_dev;
    mcp_dev = devm_kzalloc(&client->dev, sizeof(*mcp_dev), GFP_KERNEL);
    if (!mcp_dev)
        return -ENOMEM;

    mcp_dev->client = client;

    pr_info("Set private data serial\n");
    i2c_set_clientdata(client, mcp_dev);

    return 0;

}

remove()

This function is called when the driver is being unregistered.

static int mcp9808_i2c_remove(struct i2c_client *client)
{
    pr_info("mcp9808_i2c_remove is called.\n");
    return 0;
}

Extra Client Data

The user/ client should be able to read the sensor value from the user space. In order to do that, we have to implement a function to read the sensor value to send the value back to users. To simplify implementation, I am going to use Misc subsystem API to register the device and expose read() and ioctl() operation.

The device registration using Misc subsystem API can be done in the probe() function.


static const struct file_operations fops = {
    .owner = THIS_MODULE,
    .read  = mcp9808_read
};

static int mcp9808_i2c_probe(struct i2c_client *client,
                              const struct i2c_device_id *id)
{   
    ...
    char *name;
    name = devm_kasprintf(&client->dev, GFP_KERNEL, "mcp9808-%x", (inp[0] << 8 | inp[1]));

    mcp_dev->miscdev.minor = MISC_DYNAMIC_MINOR;
    mcp_dev->miscdev.name = name;
    mcp_dev->miscdev.fops = &fops;
    mcp_dev->miscdev.parent = &client->dev;

    ...
    int ret;
    ret = misc_register(&mcp_dev->miscdev);
    pr_info("Call misc_register %d n", ret);

}

static int mcp9808_i2c_remove(struct i2c_client *client)
{
    pr_info("mcp9808_i2c_remove is called.\n");

    // Unregister the device
    struct mcp9808_dev *sdev = i2c_get_clientdata(client);
    dev_info(sdev->miscdev.this_device, "Unregistering device\n");
    misc_deregister(&sdev->miscdev);

    return 0;
}

Receiving Data using I2C Communication

It is time to implement the read() function. Inside this function, we will read the register to get the temperature value. The following I2C APIs can be used to write and read registers.

int i2c_master_send()
int i2c_master_recv()

// A helper function
int i2c_transfer()
static int mcp9808_read_registers(struct i2c_client *client,
                                  unsigned char reg_addr,
                                  u8* inp, size_t size)
{
    int error;
    struct i2c_msg msg[2];

    msg[0].addr = client->addr;
    msg[0].flags = 0;
    msg[0].len = 1;
    msg[0].buf = &reg_addr;

    msg[1].addr = client->addr;
    msg[1].flags = I2C_M_RD;
    msg[1].len = size;
    msg[1].buf = inp;

    error = i2c_transfer(client->adapter, msg, 2);
    pr_info("i2c_transfer error: %d\n", error);
    if (error >= 0) 
    {
        return error;
    }

    return error;
}

ssize_t mcp9808_read(struct file *f, char __user *buf, size_t sz, loff_t *off)
{
    struct mcp9808_dev *sdev = container_of(f->private_data, struct mcp9808_dev, miscdev);
    if (sdev == NULL) 
    {
        pr_info("Empty sdev\n");
        return -1;
    }

    if (sdev->client == NULL)
    {
        pr_info("client is NULL\n");
        return -1;
    }

    int error;
    u8 inp[2];
    error = mcp9808_read_registers(sdev->client, 0x05, inp, sizeof(inp));
    if (error >= 0)
    {
        int temperature = ((inp[0] & 0x0f) << 4) + (inp[1] >> 4);
        pr_info("Temperature = %d.%d oC\n", temperature, inp[1] & 0x0f);
    }

    int nbytes = 2 - copy_to_user(buf, inp, 2);
    *off = nbytes;

    pr_info("Reading function, nbytes=%d, pos=%d, sz=%d\n", nbytes, (int) *off, sz);

    return 0;
}

Testing

Compile the kernel device driver and load it on the device

# insmod temperature.ko

# ls /dev/mcp9808-54
/dev/mcp9808-54

# cat /dev/mcp9808-54
[ 6139.115536] i2c_transfer error: 2
[ 6139.118895] Temperature = 25.7 oC
[ 6139.122228] Reading function, nbytes=2, pos=2, sz=4096

# cat /dev/mcp9808-54 
[ 6139.754094] i2c_transfer error: 2
[ 6139.757453] Temperature = 24.14 oC
[ 6139.760871] Reading function, nbytes=2, pos=2, sz=4096

Did you find this article valuable?

Support Aries by becoming a sponsor. Any amount is appreciated!