Pages

Wednesday, June 11, 2014

Implementing a device driver for the MPL3115a on the Raspberry Pi Part II

In my previous blog, I noticed irregularities with my handheld digital temperature measurement and the temperature measurement coming out of the device driver. The first one was obvious - I initially thought that the output by default would be in degrees Fahrenheit, but upon consulting the manual, it was indeed in degrees Centigrade.

Another this was that in calculating for the decimal part of the temperature, the default computation that was included in the Python code was buggy. The python code that comes with the Xtrinsic Sensor board was ("mpl3115a2.py"):

        def getTemp(self):
                t = self.readTemp()
                t_m = (t >> 8) & 0xff;
                t_l = t & 0xff;

                if (t_l > 99):
                        t_l = t_l / 1000.0
                else:
                        t_l = t_l / 100.0
                return (t_m + t_l)

So the python code simply divides the LSB with 100 or 1000 depending on its value. However this is not the case as presented in the specs:



So the value needs to be divided by 256 (instead of 100 or 1000) to get the decimal part.

Notice also that in kernel space, we need to avoid floating point math, so in order to display the decimal part of the temperature:


    // Convert the temperature data to display in
    // decimal
    temp_valmsb = (data->tempval >> 8 ) & 0xFF;
    isneg = (temp_valmsb > 0x7F);
    // Get the 2's complement
    if (isneg)
        temp_valmsb = ~temp_valmsb + 1;
    temp_vallsb = ((data->tempval & 0xFF) * 100) >> 8;

    return sprintf(buf, "%d:%d:%d:%ld|%c%d.%02d\n",
        tm_lr.tm_hour,
        tm_lr.tm_min,
        tm_lr.tm_sec,
        data->last_update_time.tv_usec,
        isneg ? '-':'+',
        temp_valmsb,
        temp_vallsb);

We have to multiply the value by 100 (to turn the decimal part to whole numbers), the downshift with 8 has the same effect with dividing by 256.

Also an improvement with this driver is the addition of a Kernel FIFO in /proc, to see this in action:


pi@raspberrypi:/proc$ cat mpl3115_fifo
pi@raspberrypi:/proc$ cat mpl3115_fifo
09:55:10:56771|+30.81|6425824
09:55:11:25039|+30.87|6425920
09:55:11:993333|+30.87|6425936
09:55:12:961640|+30.87|6425664
09:55:13:929968|+30.87|6425808
09:55:14:898250|+30.87|6425760
09:55:15:866540|+30.87|6425568
pi@raspberrypi:/proc$ cat mpl3115_fifo
09:55:16:834797|+30.87|6425568
09:55:17:803146|+30.87|6425760
09:55:18:771415|+30.87|6425824
pi@raspberrypi:/proc$ cat mpl3115_fifo
09:55:19:739690|+30.87|6425824
09:55:20:707925|+30.87|6425760
09:55:21:676377|+30.87|6425728
pi@raspberrypi:/proc$ cat mpl3115_fifo
09:55:22:644368|+30.87|6425616
09:55:23:612579|+30.87|6425440
pi@raspberrypi:/proc$ cat mpl3115_fifo
09:55:24:580740|+30.87|6425712
09:55:25:549033|+30.87|6425488
09:55:26:517295|+30.87|6425728
09:55:27:485592|+30.87|6425888

So the driver now has a way to store the read values from the MEMS sensor board and stores it in the kernel fifo, this is necessary if you don't want to miss data from the driver. Note however that the isr can fail writing data into the fifo if the fifo is full. The consumer (user space app) must threfore continue to read elements in the fifo.

I've attached the whole source code for this driver below:


/*
 * Chip I2C Driver
 *
 * Copyright (C) 2014 Vergil Cola (vpcola@gmail.com)
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 2 of the License.
 *
 * This driver shows how to create a minimal i2c driver for Raspberry Pi.
 * The arbitrary i2c hardware sits on 0x21 using the MCP23017 chip. 
 *
 * PORTA is connected to output leds while PORTB of MCP23017 is connected
 * to dip switches.
 *
 */

#define DEBUG 1

#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/interrupt.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/jiffies.h>
#include <linux/i2c.h>
#include <linux/mutex.h>
#include <linux/err.h>
#include <linux/sysfs.h>
#include <linux/device.h>
#include <linux/gpio.h>
#include <linux/time.h>
#include <linux/kfifo.h>
#include <linux/proc_fs.h>

#include "bartemp.h"


#define MPL3115_DEVICE_NAME    "mpl3115"

static const unsigned short normal_i2c[] = { 0x60, I2C_CLIENT_END };

/* Our drivers id table */
static const struct i2c_device_id mpl3115_i2c_id[] = {
    { "mpl3115", 0 },
    {}
};

MODULE_DEVICE_TABLE(i2c, mpl3115_i2c_id);

#define FIFO_SIZE   512 

#define PROC_FIFO   "mpl3115_fifo"

/* lock for procfs read/write access */
static DEFINE_MUTEX(read_lock);

/* Our fifo for storin sensor values */
static struct kfifo tempval_fifo;
 
/* Each client has that uses the driver stores data in this structure */
struct mpl3115_data {
    struct i2c_client * client;
    short int irq_gpio17;
    bool     isaltmode;
    struct timeval last_update_time;
    uint32_t    tempval;
    uint32_t    altbarval;
    int kind;
};

/* Define the GPIO that cuases the interrupt */
#define GPIO_INT_GPIO17     17

/* The description of this interrupt */
#define GPIO_INT_GPIO17_DESC    "MPL3115 Sensor Interrupt"


/* Our driver attributes/variables are currently exported via sysfs. 
 * For this driver, we export two attributes - chip_led and chip_switch
 * to correspond to MCP23017's PORTA (led) and PORTB(dip switches).
 *
 * The sysfs filesystem is a convenient way to examine these attributes
 * in kernel space from user space. They also provide a mechanism for 
 * setting data form user space to kernel space. 
 **/
static ssize_t set_altbar_mode(struct device *dev, 
        struct device_attribute * devattr,
        const char * buf, 
        size_t count)
{
    struct i2c_client * client = to_i2c_client(dev);
    struct mpl3115_data * data = i2c_get_clientdata(client);

    dev_dbg(&client->dev, "%s with [%c], current mode [%c]\n", 
        __FUNCTION__,
        buf[0],
        (data->isaltmode ? 'A' : 'B')
        );

    /* Determine if we use B - barometer or A - Altitude
     * mode. We only take the first char, discard all other
     * values of the buffer
     */
    if ((buf[0] != 'A') && (buf[0] != 'B'))
        return -EINVAL;

    disable_irq(data->irq_gpio17);
    // We need to disable interrups from generating on the device
    dev_info(&client->dev, "disabling interrupts on the device\n");
    disable_drdy_interrupt(client);
    mpl_standby(client);
    // Call init to initialize in barometer mode
    data->isaltmode = (buf[0] == 'A');
    // Calling init again will activate the device
    init(client, data->isaltmode); // true - altitude mode, false - barometer mode


    dev_dbg(&client->dev, "%s: Setting altitude/barometer mode to [%s]\n", 
            __FUNCTION__,
            data->isaltmode ? "Altitude":"Barometer");

    // re-enable interrupts again
    enable_irq(data->irq_gpio17);

    return count;
}

static ssize_t get_altbar_mode(struct device *dev, 
        struct device_attribute * devattr,
        char * buf)
{
    struct i2c_client * client = to_i2c_client(dev);
    struct mpl3115_data * data = i2c_get_clientdata(client);

    return sprintf(buf, "%c\n", data->isaltmode ? 'A' : 'B');
}


static ssize_t get_altbar_value(struct device *dev, 
        struct device_attribute * devattr,
        char * buf)
{
    struct tm tm_lr;
    struct i2c_client * client = to_i2c_client(dev);
    struct mpl3115_data * data = i2c_get_clientdata(client);

    // We print the last time the value was read 
    time_to_tm(data->last_update_time.tv_sec, 0, &tm_lr);

    // Convert the raw altitude data to 
    // alt/barr type

    return sprintf(buf, "%d:%d:%d:%ld|%d\n", 
        tm_lr.tm_hour, 
        tm_lr.tm_min, 
        tm_lr.tm_sec, 
        data->last_update_time.tv_usec, 
        data->altbarval);

}

/* The value read by the device driver is actually
 * a 16 bit value. Since we can not do floating point
 * in kernel space, user space can use this value
 * to get the value in Celcius using the formula
 *
 * MSB = (value >> 8) & 0xFF;
 * LSB = (value & 0xFF;
 *
 * LSB = (LSB > 99) (LSB/1000) : (LSB/100);
 *
 * Celcius = MSB + LSB;
 */
static ssize_t get_temp_value(struct device *dev, 
    struct device_attribute *dev_attr,
    char * buf)
{
    struct tm tm_lr;
    struct i2c_client * client = to_i2c_client(dev);
    struct mpl3115_data * data = i2c_get_clientdata(client);
    int temp_valmsb;
    int temp_vallsb;
    bool isneg;


    // We print the last time the value was read 
    time_to_tm(data->last_update_time.tv_sec, 0, &tm_lr);

    // Convert the temperature data to display in 
    // decimal
    temp_valmsb = (data->tempval >> 8 ) & 0xFF;
    isneg = (temp_valmsb > 0x7F);
    // If negative, get the 2's complement
    if (isneg)
        temp_valmsb = ~temp_valmsb + 1;

    temp_vallsb = ((data->tempval & 0xFF) * 100) >> 8;

    return sprintf(buf, "%d:%d:%d:%ld|%d.%02d\n", 
        tm_lr.tm_hour, 
        tm_lr.tm_min, 
        tm_lr.tm_sec, 
        data->last_update_time.tv_usec, 
        temp_valmsb,
        temp_vallsb);
}

/* attribute to set/get the altitude or barometer mode */
static DEVICE_ATTR(altbarmode, S_IWUGO | S_IRUGO, get_altbar_mode, set_altbar_mode);
/* attribute to read the altitue/barometer value */
static DEVICE_ATTR(altbarvalue, S_IRUGO, get_altbar_value, NULL);
/* attribute to read the temperature value */
static DEVICE_ATTR(tempvalue, S_IRUGO, get_temp_value, NULL);



/* Our ISR function. Here we can safely communicate with the 
 * I2C bus.
 */
static irqreturn_t gpio17_isr(int irq, void * dev_id)
{
    struct mpl3115_data * data = dev_id;
    struct i2c_client * client = data->client;
    struct tm tm_lr;
    unsigned char buff[100];
    uint32_t temp_value, alt_value, len;
    uint32_t temp_valmsb, temp_vallsb;
    bool isneg = false;

    // Retrieve data from the device, update the client data
    // and the last time read
    //dev_info(&client->dev, "Interrupt called!\n");
   
    // First check if the data is coming from DYRDY
    if (is_data_ready_set(client))
    {
        //dev_info(&client->dev, "Data ready set, reading temperature and %s values!\n",
        //    data->isaltmode ? "altitude" : "barometric");
        // Read temperature sensor
        temp_value = read_temp(client);
        alt_value = read_altbar(client);

        // Update the last read time
        do_gettimeofday(&(data->last_update_time));

        // Push the temperature value into our fifo
        time_to_tm(data->last_update_time.tv_sec, 0, &tm_lr);

        temp_valmsb = (temp_value >> 8) & 0xFF;
        isneg = (temp_valmsb > 0x7F);
        // If negative, get the 2's complement
        if (isneg)
            temp_valmsb = ~temp_valmsb + 1;

        temp_vallsb = ((temp_value & 0xFF) * 100) >> 8;


        len = sprintf(buff, "%02d:%02d:%02d:%ld|%c%d.%02d|%d\n", 
            tm_lr.tm_hour, 
            tm_lr.tm_min, 
            tm_lr.tm_sec, 
            data->last_update_time.tv_usec, 
            (isneg) ? '-':'+',
            temp_valmsb,
            temp_vallsb,
            alt_value
            );

        //dev_info(&client->dev, "writing [%s] with len [%d] to fifo\n",
        //    buff,
        //   len);
        if (kfifo_len(&tempval_fifo) >= FIFO_SIZE)
            dev_err(&client->dev, "Error: fifo is full, may loose temp data!\n");

        // continue putting data, disregard if fifo is full
        kfifo_in(&tempval_fifo, buff, len);

        // Update data
        data->tempval = temp_value;
        data->altbarval = alt_value;
    }

    return IRQ_HANDLED;

}


/* The following functions are callback functions of our driver. 
 * Upon successful detection of kernel (via the chip_detect function below). 
 * The kernel calls the chip_i2c_probe(), the driver's duty here 
 * is to allocate the client's data, initialize
 * the data structures needed, and to call chip_init_client() which
 * will initialize our hardware. 
 *
 * This function is also needed to initialize sysfs files on the system.
 */
static int mpl3115_i2c_probe(struct i2c_client *client,
    const struct i2c_device_id *id)
{
    struct device * dev = &client->dev;
    struct mpl3115_data *data = NULL;
    int error;

    dev_info(&client->dev, "%s\n", __FUNCTION__);

    /* Allocate the client's data here */
    data = kzalloc(sizeof(struct mpl3115_data), GFP_KERNEL);
    if(!data)
    {
        dev_err(&client->dev, "Error allocating memory!\n");
        return -ENOMEM;
    }

    /* Initialize client's data to default */
    data->client = client; // Loopback
    data->kind = id->driver_data;
    data->tempval = 0;
    data->altbarval = 0;

    /* initialize our hardware */
    data->isaltmode = false;
    init(client, data->isaltmode);

    if ((error = gpio_request(GPIO_INT_GPIO17, GPIO_INT_GPIO17_DESC)) < 0)
    {
        dev_err(&client->dev,"GPIO request failure\n");
        goto error_freemem;
    }

    if ((data->irq_gpio17 = gpio_to_irq(GPIO_INT_GPIO17)) < 0)
    {
        dev_err(&client->dev,"GPIO to IRQ mapping failure\n");
        error = data->irq_gpio17;
        goto error_freegpio;
    }

    dev_info(&client->dev, "Mapped interrupt %d\n",
        data->irq_gpio17);

    i2c_set_clientdata(client, data);
    
    error = request_threaded_irq(data->irq_gpio17, NULL,
        gpio17_isr,
        IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
        client->name, data);
    if (error < 0)
    {
        dev_err(&client->dev, "Unable to request IRQ\n");
        goto error_freegpio;
    }

    // We now register our sysfs attributs. 
    device_create_file(dev, &dev_attr_altbarmode);
    device_create_file(dev, &dev_attr_altbarvalue);
    device_create_file(dev, &dev_attr_tempvalue);


    return 0;

error_freegpio:
    gpio_free(GPIO_INT_GPIO17);

error_freemem:
    kfree(data);
    return error;

}

/* This function is called whenever the bus or the driver is
 * removed from the system. We perform cleanup here and 
 * unregister our sysfs hooks/attributes.
 **/
static int mpl3115_i2c_remove(struct i2c_client * client)
{
    struct device * dev = &client->dev;
    struct mpl3115_data * data = i2c_get_clientdata(client);

    dev_info(&client->dev, "%s\n", __FUNCTION__);

    disable_irq(data->irq_gpio17);

    // We need to disable interrups from generating on the device
    dev_info(&client->dev, "disabling interrupts on the device\n");
    disable_drdy_interrupt(client);
    mpl_standby(client);

    dev_info(&client->dev, "freeing irq %d\n", data->irq_gpio17);
    free_irq(data->irq_gpio17, data);


    dev_info(&client->dev, "removing sys entries\n");
    device_remove_file(dev, &dev_attr_altbarmode);
    device_remove_file(dev, &dev_attr_altbarvalue);
    device_remove_file(dev, &dev_attr_tempvalue);


    // we first need to free the gpio lines
    gpio_free(GPIO_INT_GPIO17);
    dev_info(&client->dev, "freeing gpio lines\n");

    // Finally free the data allocated by the device
    dev_info(&client->dev, "freeing kernel memory\n");
    kfree(data);

    dev_info(&client->dev, "driver removed\n");

    return 0;
}

/* This callback function is called by the kernel 
 * to detect the chip at a given device address. 
 * However since we know that our device is currently 
 * hardwired to 0x21, there is really nothing to detect.
 * We simply return -ENODEV if the address is not 0x21.
 */
static int mpl3115_i2c_detect(struct i2c_client * client, 
    struct i2c_board_info * info)
{
    struct i2c_adapter *adapter = client->adapter;
    int address = client->addr;
    const char * name = NULL;

    dev_info(&client->dev,"%s\n", __FUNCTION__);

    if (!i2c_check_functionality(adapter, I2C_FUNC_SMBUS_BYTE_DATA))
        return -ENODEV;

    // Since our address is hardwired to 0x21
    // we update the name of the driver. This must
    // match the name of the chip_driver struct below
    // in order for this driver to be loaded.
    if ((address == MPL3115A2_IIC_ADDRESS)
        && is_valid_device(client))
    {
        name = MPL3115_DEVICE_NAME;
        dev_info(&adapter->dev,
            "Chip device found at 0x%02x\n", address);
    }else
        return -ENODEV;

    /* Upon successful detection, we coup the name of the
     * driver to the info struct.
     **/
    strlcpy(info->type, name, I2C_NAME_SIZE);
    return 0;
}


/* This is the main driver description table. It lists 
 * the device types, and the callback functions for this
 * device driver
 **/
static struct i2c_driver mpl3115_driver = {
    .class      = I2C_CLASS_HWMON,
    .driver = {
            .name = MPL3115_DEVICE_NAME,
    },
    .probe          = mpl3115_i2c_probe,
    .remove         = mpl3115_i2c_remove,
    .id_table       = mpl3115_i2c_id,
    .detect         = mpl3115_i2c_detect,
    .address_list   = normal_i2c,
};

static ssize_t fifo_read(struct file * file,
    char __user *buf,
    size_t count,
    loff_t * ppos)
{
    int retval;
    unsigned int copied;

    if (mutex_lock_interruptible(&read_lock))
        return -ERESTARTSYS;

    retval = kfifo_to_user(&tempval_fifo, buf, count, &copied);

    mutex_unlock(&read_lock);

    return retval ? retval : copied;                            

}

/* FIFO operations of procfs */
static const struct file_operations fifo_fops = {
    .owner = THIS_MODULE,
    .read = fifo_read,
    .llseek = noop_llseek,
};

/* Entry and exit functions */
static int __init mpl3115_init(void)
{
    /* Create our kfifo here */
    int retval;

    retval = kfifo_alloc(&tempval_fifo, FIFO_SIZE, GFP_KERNEL);
    if (retval)
    {
        printk(KERN_ERR "Error in kfifo_alloc\n");
        return retval;
    }

    if (proc_create(PROC_FIFO, 0, NULL, &fifo_fops) == NULL)
    {
        kfifo_free(&tempval_fifo);
        return -ENOMEM;
    }

    return i2c_add_driver(&mpl3115_driver);
}
module_init(mpl3115_init);

static void __exit mpl3115_exit(void)
{
    remove_proc_entry(PROC_FIFO, NULL);
    kfifo_free(&tempval_fifo);

    return i2c_del_driver(&mpl3115_driver);
}
module_exit(mpl3115_exit);

MODULE_AUTHOR("Vergil Cola <vpcola@gmail.com>");
MODULE_DESCRIPTION("MPL3115a I2C Driver");
MODULE_LICENSE("GPL");


Alternatively, you can also get the source code of this driver from git here.

No comments: