Linux I2C device driver for MCP9808 Temperature sensor
Linux I2C device driver for MCP9808 Temperature sensor
Device Drivers
  1. In Linux, device drivers acts as an intermediate between the user application and hardware.
  2. The device drivers expose the functionality of the hardware to user programs.

In this post I am going to write Linux device driver for MCP9808 temperature sensor which is interfaced through I2C-2 bus with UDOO neo full board.

Hardware used in this tutorial.
  1. Udoo neo full is based on NXP® i.MX 6SoloX processor with 1Gb of DDR memory.
  2. MCP9808 temperature sensor breakout board and some jumper wires.

Here the I2C device driver for MCP9808 is compiled as out-of-tree module, which can be loaded into the target Udoo board during runtime. This driver is compiled with Freescale Linux kernel version “Kernel_4.17.x+fslc” and can be used for all kernel versions above 4.1.

Linux I2C subsystem consists of three main components

I2C bus core

Acts as interface support between an individual client driver and some I2C bus masters such as the i.MX6x I2C controllers. It manages bus arbitration, retry handling, and various other protocol details.
Files : drivers/i2c/i2c-core-base.c and i2c-core-smbus.c

I2C controller drivers

The I2C controller driver is a set of custom functions that issues read/writes to the specific I2C controller hardware I/O addresses. There is a specific code for each I2C controller of the processor.
examples i2c controller driver for IMX processors drivers/i2c/busses/i2c-imx.c

I2C device drivers also called as I2C client drivers

The driver code is specific to the device like accelerometer, digital to analog converter, and uses the I2C core API to send and receive data to/from the I2C device.
examples: drivers/

Implementation

An I2C driver is represented in the kernel as an instance of struct i2c_driver. It contains and characterizes general access routines, needed to handle the devices claiming the driver.

static struct i2c_driver mcp9808_i2c_driver = {
    .driver = {
	.owner = THIS_MODULE,
	.name = "mcp9808",
	.of_match_table = of_match_ptr(mcp98xx_of),
    },
    .probe = mcp9808_probe,
    .remove = mcp9808_remove,
    .id_table = mcp98xx_id,
};
module_i2c_driver(mcp9808_i2c_driver);

The binding will happen based on the i2c_device_id table or device tree compatible string. The I2C core first tries to match the device by compatible string (OF style, which is device tree), and if it fails, it then tries to match the device by id table.

struct of_device_id structures that hold the compatible strings supported by the driver. The .compatible strings that should store the same value of the Device Tree device node´s compatible property.

static const struct of_device_id mcp98xx_of[] = {
    {.compatible = "microchip,mcp9808",},
    { /*sentinel*/ },
};

static const struct i2c_device_id mcp98xx_id[] = {
    {"mcp9808",0},
    {},
};

In the Udoo board device tree dts/imx6sx-udoo-neo-full.dts a new node is created for MCP9808 under i2c-2 parent node. In the new node temp, reg corresponds to the i2c address of the mcp9808.

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

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

};

The driver´s probe() function is called when the compatible field in one of the of_device_id entries matches with the compatible property of a DT device node. The probe() function is responsible for initializing the device with the configuration values obtained from the matching DT device node and also registering the device to the appropriate kernel framework.

static int mcp9808_probe(struct i2c_client *client, const struct i2c_device_id *id)
{

    struct i2c_msg msg[2];
    unsigned char reg_addr = 0x06;
    u8 data[2];
    int err;
    dev_t dev_no;
    struct device *mcp_device=NULL;
    struct mcp_data *mcp;

    pr_info("%s\n",__func__);
    if(!i2c_check_functionality(client->adapter,I2C_FUNC_SMBUS_BYTE_DATA)){
	pr_err("i2c_check_functionality\n");
    }

    /* Verify the identity of  device */
    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 = 2;
    msg[1].buf = data;

    if(i2c_transfer(client->adapter,msg,2) < 0){
	pr_err("i2c_transfer fail\n MCP9808 not found\n");
    }
    pr_info("MCP9808 Manufacture ID = %x\n",data[0]<<8|data[1]);

    /* Allocate the major and minor number */
    err = alloc_chrdev_region(&dev_no,0,1,DEVICE_NAME);
    if(err < 0){
	pr_err("alloc_chrdev_region fail\n");
	return err;
    }
    mcp_major = MAJOR(dev_no);
    mcp_minor = MINOR(dev_no);
    pr_info("%s major num = %d -- minor num = %d\n",DEVICE_NAME,mcp_major,mcp_minor);

    /* Create the sysfs class */
    mcp_class = class_create(THIS_MODULE,DEVICE_NAME);
    if(IS_ERR(mcp_class)){
	err = PTR_ERR(mcp_class);
	goto fail;
    }

    /* Allocating memory for device specific data */
    mcp = kmalloc(sizeof(struct mcp_data),GFP_KERNEL);
    if(mcp == NULL){
	pr_err("kmalloc fail\n");
	err = -ENOMEM;
	return err;
    }

    mcp->data = NULL;
    mcp->client = client;
    
    /* Initilize and register character device with kernel */
    cdev_init(&mcp->dev,&mcp_fops);
    mcp->dev.owner = THIS_MODULE;
    err = cdev_add(&mcp->dev,dev_no,1);

    if(err){
	pr_err("Adding cdev fail\n");
	goto fail;
    }

    /* create the device so that user can access the slave device from user space */
    mcp_device = device_create(mcp_class,
		NULL,
		dev_no,
		NULL,
		DEVICE_NAME);

    if(IS_ERR(mcp_device)) {
	err = PTR_ERR(mcp_device);
	pr_err("Device creation fail\n");
	cdev_del(&mcp->dev);
	goto fail;
    }
    i2c_set_clientdata(client,mcp);
   return 0; 

fail:
   /* if anything fails undo everything */
    if(mcp_class != NULL){
	class_destroy(mcp_class);
    }
    unregister_chrdev_region(MKDEV(mcp_major,mcp_minor),1);
    if(mcp != NULL){
	kfree(mcp);
    }
    return err;

}

Probe function will implements following

  1. Reserve a major and a range of minors
  2. Create a class for your devices visible in /sys/class/.
  3. Set up file operations for users to access using standard file manipulation functions.
  4. Making the device live for the users to access
  5. Creating a device node each device
  6. Setting the device-specific data. The I2C client (which represents the device itself) is represented by a struct i2c_client structure.

The remove function used to unregister the device driver from the subsystem where we have registered in the probe() and also to clear the memory that has been assigned to driver during initilization.

static int mcp9808_remove(struct i2c_client *client)
{
    struct mcp_data *mcp = i2c_get_clientdata(client);
    device_destroy(mcp_class,MKDEV(mcp_major,mcp_minor));
    kfree(mcp);
    class_destroy(mcp_class);
    unregister_chrdev_region(MKDEV(mcp_major,mcp_minor),1);
    return 0;
}
File operations

Used to expose the driver’s methods to the user-space so that users can use this driver using standard system calls. The kernel driver will use the specific functions copy_from_user() and copy_to_user() to exchange data with userspace.

static struct file_operations mcp_fops = {
    .owner = THIS_MODULE,
    .open = mcp_open,
    .release = mcp_release,
    .read = mcp_read,
    .unlocked_ioctl = mcp_ioctl,
};
  1. The mcp_open function will retrive the device specific data with help of container_of() macro. In mcp_open function, memory will be assigned for data read and write with the device.
  2. mcp_open will execute for each open call from userspace.
  3. In mcp_release the memory assigned in mcp_open will be deleted.
  4. mcp_ioctl is used to implement mcp9808 shutdown and wakeup by using commands from userspace programs using standard ioctl functions.
ssize_t mcp_read(struct file *filp,char __user *buf,size_t count,loff_t *f_pos)
{
   u8 reg_addr;
   struct i2c_msg msg[2];
   struct mcp_data *mcp = filp->private_data;
   u8 rawtmp[2];

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

    msg[1].addr = mcp->client->addr;
    msg[1].flags = I2C_M_RD;
    msg[1].len = 2;
    msg[1].buf = rawtmp;

    if(i2c_transfer(mcp->client->adapter,msg,2) < 0){
	pr_err("i2c_transfer fail in %s\n",__func__);
    }

    mcp->data[0] = ((rawtmp[0] & 0x0f) << 4) | (rawtmp[1] >> 4);
    mcp->data[1] = rawtmp[1] & 0x0f;

    pr_info("Temprature value = %d.%d\n",mcp->data[0],mcp->data[1]);
    
    if(copy_to_user(buf,mcp->data,2) != 0){
	pr_err("copy_to_user fail\n");
	return -EIO;
    }
    return 2;
}

In the mcp_read function, the raw temperature value will be read and converted to the actual temperature value and copied to userspace with help of the copy_to_user() function.

To compile the module type “make” inside the mcp9808 folder then mcp9808.ko file will be generated in same folder copy it to target board Udoo. To load the module into the kernel
$ insmod ./mcp9808.ko
$ dmesg
[ 48.450364] mcp9808_probe
[ 48.460300] MCP9808 Manufacture ID = 54
[ 48.468824] mcp9808 major num = 242 -- minor num = 0
[ 109.496700] Temprature value = 28.3
[ 114.766716] Temprature value = 28.5
[ 117.098075] Temprature value = 28.3
Simple user application to interact with mcp9808 kernel module
int main(void)
{
    int file;
    char filename[] = "/dev/mcp9808";
    char temp[2],ch;

    file = open(filename,O_RDWR);
    if(file < 0){
	printf("Coudn't open /dev/mcp9808\n");
	exit(0);
    }

    ioctl(file,MCP_WKEUP);
    do
    {
	memset(temp,0,2);
	if(read(file,temp,2) != 2){
	    printf("Read fail\n");
	}
	printf("Temperature value = %d.%d\n",temp[0],temp[1]);
	printf("Press y|Y to read another value = ");	
	scanf("%c",&ch);
	getchar();
    }while(ch == 'y' || ch == 'Y');

    ioctl(file,MCP_SHUTDWN);
    close(file);
    return 0;
}

Once the driver successfully loaded then /dev/mcp9808 device file will be created. User-space programs can use standard file operation function like open, close, read, write to interact with device drivers. This user-pace program will read the temperature value and print it on the console

root@jagguudoo:~# ./mcpUser
Temperature value = 28.3
Press y|Y to read another value = y
Temperature value = 28.5

Source code is available at GitHub link : https://github.com/jaggu777/mcp9808