Pages

Wednesday, August 27, 2014

Weather Info project with CC3000, ILI9341 and DHT11

It has been ages since my last post. I took the time for vacation to take care of family matters. Now that I have time to kill, I was able to come up with this simple project that involves Sparkfun's CC3000 wifi sheild, an ILI9341 2.5 inch TFT LCD, an inexpensive DHT11 humidity and temperature sensor, and of course the handy Nucleo STM32F401RE.

In this project, I've chosen the Nucleo board since it would be more challenging to run something on a limited 96K RAM and 512K of flash ROM. In comparison, my Raspberry Pi has 512MB and code size limited only by the size of your SD card. The only advantage I think that the Nucleo has in comparison to the Raspberry Pi are the IO's - particulary the SPI channels. There are 3 SPI channels on the Nucleo board compared to only 1 on the Raspberry Pi header. The bulk of the challenge lies on having to control all these IO with an embedded RTOS, running different threads with a limited code size and ram. Luckily, the CC3000 has an embedded TCP/IP stack that runs on it, this means that I wont have to bloat code with an lwIP. However, the CC3000 itself is a beast to debug over SPI and TI's supplied code for controlling it would sometimes go into an exception handler. I initially created this project on embed (online IDE), with embed's own embed-rtos as the RTOS of choice. However since I usually work offline most of the time, I moved the code to work on Keil MDK.

So to summarize this project, the board can be booted either in two ways - by pressing the boot switch on during boot, the Nucleo board enters into a command shell (via the built-in serial RS232 connection), where you can enter a bunch of commands to test the LCD, examine memory or other tasks. By default, by remaining the boot switch open during boot, it goes into its normal process loop which consists of the following:
  1. Load config.ini from the SD card, and get the web page to get the weather info from. In this case this would be openweathermap.com. 
  2. Initializes the CC3000 wifi sheild and connects to the Access point (AP).
  3. Initialize the LCD (ILI9341)
  4. Once AP is connected, it proceeds to read the weather data from openweathermap.com, and retrieve the JSON formatted weather info.
  5. Parse the JSON file and extract relevant weather data.
  6. Read the values of the DHT11 sensor - temperature and humidity.
  7. Once all weather info's are read update the weather information on the LCD screen.
  8. Repeat the process from step 4.
Here's the video of the running demonstration:


Links to souce code is found here : https://github.com/vpcola/Nucleo

That's it for now, I'll discuss the details of the implementation of this project on my next blog.

Friday, June 13, 2014

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

In my previous posts I was working on an MPL3115a device driver for the raspberry pi. The purpose of this device driver is really just a learning opportunity, I did not intend it to be used for some sort of project or anything else, but somehow - this one has come to take a life of its own.  One of the things that has come to my mind is to create some sort of a home automation project, where one can monitor his/her rooms temperature via his cellfone or over the internet. The second part of this project may include some IR transmitter to control some air conditioning units perhaps. These things will all be controlled from the raspberry pi.

Of course the device driver may not be necessary, since the temperature sensor can be controlled directly from user space through i2c. But for the home automation project, there may be instances where we wan't the raspberry pi to sleep - wake up on LAN access, or when the temperature threshold goes beyond a pre-set value. Doing these things via a userland application will require pooling the sensor every now and then, and thus can't effectively put the raspberry pi to sleep. It was for this reason that I made a third installment of the MPL3115 device driver, In this driver the following changes were made:

  • Provide a char device functionality (/dev/mpl3115)
  • Expose the step time (time between sensor reads) in /sys.
  • Sleep the device when not in use and wake only on interrupt.
  • Provide correct readings for altimeter (in meters) and barometric pressure (in pascals)
    as opposed to just raw values being displayed.
Another difference with the previous driver was the lack of the stamp time, I initially planned that the time will have to be taken from the user app, and not in kernel space.

A snapshot of this running device driver is shown below. Notice that /dev/mpl3115 is currently accessible only as root. 


pi@raspberrypi ~ $ sudo insmod mpl3115.ko
pi@raspberrypi ~ $ sudo cat /dev/mpl3115
30.93 C|100620.00 P
30.93 C|100618.25 P
30.93 C|100615.25 P
31.00 C|100614.50 P
30.93 C|100612.25 P
31.00 C|100611.75 P
31.00 C|100613.25 P
30.93 C|100609.75 P
31.00 C|100611.00 P
30.93 C|100607.25 P
30.93 C|100605.75 P
31.00 C|100611.50 P
30.93 C|100606.25 P
^Cpi@raspberrypi ~ $ 
pi@raspberrypi ~ $ cat /sys/bus/i2c/drivers/mpl3115/1-0060/altbarmode
B
pi@raspberrypi ~ $ echo "A" > /sys/bus/i2c/drivers/mpl3115/1-0060/altbarmode
pi@raspberrypi ~ $ cat /sys/bus/i2c/drivers/mpl3115/1-0060/altbarmode
A
pi@raspberrypi ~ $ sudo cat /dev/mpl3115
30.93 C|61.8750 M
30.93 C|61.7500 M
30.93 C|61.8750 M
30.93 C|61.6875 M
30.93 C|61.6250 M
30.93 C|61.5000 M
30.93 C|61.6875 M
30.93 C|61.7500 M
30.93 C|61.8750 M
30.93 C|62.0000 M
30.93 C|61.5625 M
30.93 C|62.1250 M
30.93 C|61.6250 M
30.93 C|61.7500 M
30.93 C|61.8125 M
30.93 C|61.5000 M

Among the many changes is the "C" centigrade postfix and the "M"/"P" postfix for meters or pacals.

The driver also now exposes the step time in /sys. This controls how often our interrupt is invoked. The step time corresponds to the sensors step time register which is expressed in 
2 ^ x in seconds. Therefore when you wan't the device to take temperature readings every 2 seconds, set the step time to 1 (since 2 ^ 1 = 2), set to 2 for every 4 seconds ( 2 ^ 2 = 4), and so on. By default, the value of the step time register is 0 (which explains why temperature readings are taken every second).


pi@raspberrypi ~ $ cat /sys/bus/i2c/drivers/mpl3115/1-0060/steptime
0
pi@raspberrypi ~ $ echo "1" > /sys/bus/i2c/drivers/mpl3115/1-0060/steptime
pi@raspberrypi ~ $ cat /sys/bus/i2c/drivers/mpl3115/1-0060/steptime
1
pi@raspberrypi ~ $ sudo cat /dev/mpl3115
30.93 C|61.7500 M
30.93 C|61.7500 M
30.93 C|61.8750 M
30.93 C|61.8125 M
30.93 C|62.0000 M
30.93 C|62.1250 M
30.93 C|61.5000 M
30.93 C|61.8750 M
30.93 C|62.0000 M
30.93 C|61.9375 M
30.93 C|62.0000 M
30.93 C|61.8125 M


The source code for this driver is shown 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/wait.h>
#include <linux/fs.h>
#include <linux/poll.h>
#include <linux/string.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 

/* We start by defining our device variables */
static struct semaphore sem;
static wait_queue_head_t inq;
static struct kfifo tempval_fifo;
static struct device * mpl3115_device = NULL;
static struct class * mpl3115_device_class = NULL;
static int mpl3115_major = 0;

#define STR_VAL_SIZE 30

/* Each client has that uses the driver stores 
 * data in this structure. The information
 * hare is about the i2c client device and its
 * related data/information.
 **/
struct mpl3115_data {
    struct i2c_client * client;
    short int irq_gpio17;
    bool     isaltmode;
    // struct timeval last_update_time;
    char    last_temp_value[STR_VAL_SIZE];
    char    last_altbar_value[STR_VAL_SIZE];
};

/* 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"

/**
 * After consulting the specs, I now know how we 
 * would be able to represent the altimeter values.
 *
 * The format_altbar_value formats the altimeter/
 * barometer value and outputs the str in the buff
 * parameter. Altimeter emits the value in meters
 * while the barometric pressure is expressed in 
 * pascals.
 **/
size_t format_altbar_value(bool isaltmode, 
    uint32_t value,
    unsigned char * buff,
    size_t bufsiz)
{
    int32_t dec_meters = 0, int_meters = 0;
    uint32_t pascals_int = 0, pascals_dec = 0;
    uint16_t msbcsb = 0;
    uint8_t  lsb = 0;
    // Altimeter/Barometer value is stored
    // in 20 bits.
    //
    // In altitude mode, the format is Q16.4,
    // Integer part in meters is MSB.CSB while
    // LSB contains only bits 7-4 (upper nibble),
    // bits 3-0 is not used.
    //
    // For barometer mode, the format is still
    // 20 bits but in Q18.2 format. The integer
    // part is in MSB.CSB.Upper two bits of LSB.
    // The fractional part is in two bits 5-4
    // in the LSB.
    if (isaltmode)
    {
        // Shift down the value by 8 to get only
        // MSB.CSB (illiminating the LSB) to get the
        // integral part.
        msbcsb = (value >> 8) & 0xFFFF;
        // Once shifted down, compare if the last bit
        // is set, this will tell us that the value
        // is negative. If negative then get the 2's
        // complement
        int_meters = (msbcsb > 0x7FFF) ? 
            (~msbcsb + 1) : msbcsb;

        // Now get the fractional part which is in
        // the lsb, but only the high nibble is 
        // significant, so we shift down by 4.
        lsb = (value & 0xFF) >> 4;
        // altimeter has a resolution of .0625
        // for each increment, so we accumulate
        // if each bit is set.
        if (lsb & 0x1) dec_meters += 625;   // bit 1
        if (lsb & 0x2) dec_meters += 1250;  // bit 2
        if (lsb & 0x4) dec_meters += 2500;  // ..
        if (lsb & 0x8) dec_meters += 5000;  // bit 3

        return snprintf(buff, bufsiz, "%d.%04d M", int_meters, dec_meters);
    }
    else
    {
        // To get the integral part, we need to shift
        // down by 6 to get the MSB.CSB.Upper 2 bits.
        // The pressure value is in pascals.
        pascals_int = (value >> 6) & 0x3FFFF;
        // Fractional part is in bits 5 & 4 in the lsb,
        // so we mask the high nibble by 0x30 (leaving
        // only bits 5-4, and shift down by 4.
        lsb = (value & 0x30) >> 4;
        // Pascals dec part has a resolution of .25
        // We conver this to hundreths.
        if (lsb & 0x01) pascals_dec += 25; // bit 1
        if (lsb & 0x02) pascals_dec += 50; // bit 2

        return snprintf(buff, bufsiz, "%d.%02d P", pascals_int, pascals_dec);
    }
}

size_t format_temperature_value( 
        uint32_t value,
        unsigned char * buff,
        size_t bufsiz)
{
    int8_t temp_valmsb;
    uint8_t temp_vallsb;
    
    temp_valmsb = (value >> 8) & 0xFF;

    // The dec part resolution is by factor of 256.
    // but since we need to represent the decimal 
    // part in hundreths, we multiply it by a 
    // hundred first, then shifting by 8 (divide by
    // 256) will get the smae effect.
    temp_vallsb = ((value & 0xFF) * 100) >> 8;

    // Copy the temperature value
    return snprintf(buff, bufsiz, "%d.%02d C",
            temp_valmsb,
            temp_vallsb);
}


/* Our driver attributes/variables are currently exported via sysfs. 
 * For this driver, we export the following attributes
 *
 * altbarmode - (R/W) sets the current altimeter/barometer mode.
 * altbarvalue - The value of the altimeter/barometer in meters/pascals.
 * tempvalue - The temperature value in centigrade.
 **/
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;
}

/* Gets the altimeter mode. 'B' stands for barometric pressure
 * 'A' for altimeter.
 */
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');
}

/**
 * Sets the step time - the time interval between
 * sensor reads.  By default, the step time interval
 * is expressed as 2^x seconds, where is x is the value
 * passed into control register 2 of the sensor.
 *
 * The step time dictates how our interrupt will
 * be called periodically. Since 2^0 is 1, this explains
 * why our interrupt is called approximately every 1
 * second.
 **/
static ssize_t set_step_time(struct device *dev, 
        struct device_attribute * devattr,
        const char * buf, 
        size_t count)
{
    int err;
    int val = 0;
    struct i2c_client * client = to_i2c_client(dev);
    struct mpl3115_data * data = i2c_get_clientdata(client);

    err = kstrtoint(buf, 10, &val);
    if (err < 0)
        return err;
    /* register data only set 4 bits
     * so filter the value here
     */
    if ((val > 15) || (val < 0))
        return -EINVAL;

    disable_irq(data->irq_gpio17);
    set_devsteptime(client, val);
    // set_devsteptime calls standy, so enable it
    // after the call.
    mpl_activate(client);
    // re-enable interrupts again
    enable_irq(data->irq_gpio17);

    return count;
}

/**
 * Returns the current step time value to the user.
 **/
static ssize_t get_step_time(struct device *dev, 
        struct device_attribute * devattr,
        char * buf)
{
    struct i2c_client * client = to_i2c_client(dev);

    return sprintf(buf, "%d\n", 
        get_devsteptime(client));
}

/* Gets the value of the barometer/altimeter. The value
 * is returned to the client in terms of the mode.
 *
 * For pressure, the value is returned in pascals (postfix
 * P) appended to the actual value string. For meters, 
 * a postfix char of M is appended.
 */
static ssize_t get_altbar_value(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, "%s\n", 
        data->last_altbar_value);
}

/*
 * Returns the value of the temperature in degrees Celcius.
 * The value is postfixed with 'C'.
 */
static ssize_t get_temp_value(struct device *dev, 
    struct device_attribute *dev_attr,
    char * buf)
{
    struct i2c_client * client = to_i2c_client(dev);
    struct mpl3115_data * data = i2c_get_clientdata(client);

    return sprintf(buf, "%s\n", 
        data->last_temp_value
        );
}

/* 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 set/get the step time */
static DEVICE_ATTR(steptime, S_IWUGO | S_IRUGO, get_step_time, set_step_time);
/* TODO: implement attribute to set/get the osr rate */
// static DEVICE_ATTR(osrrate, S_IWUGO | S_IRUGO, get_osr_rate, set_osr_rate);

/* 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);

/*
 * This function is called when the device fils (/dev/mpl3115)
 * is opened. The device file is only readable, so we return
 * an error if its opened in other ways.
 */
static ssize_t fifo_open(struct inode * inode, struct file * filep)
{
    if (((filep->f_flags & O_ACCMODE) == O_WRONLY)
        || ((filep->f_flags & O_ACCMODE) == O_RDWR))
    {
        printk(KERN_WARNING "Write access is prohibited!\n");
        return -EACCES;
    }
    
    return 0;
}

/*
 * This function is called whenever the device file is read.
 * The read function simply reads the data in the kfifo
 * and return the data to the user. If the kfifo is empty,
 * we go into an interruptible wait until there is data
 * in the fifo.
 */
static ssize_t fifo_read(struct file * file,
    char __user *buf,
    size_t count,
    loff_t * ppos)
{
    int retval;
    unsigned int copied;

    if (down_interruptible(&sem))
        return -ERESTARTSYS;

    // Return busy if fifo is empty
    while(kfifo_is_empty(&tempval_fifo))
    {
        up(&sem);

        if(file->f_flags & O_NONBLOCK)
            return -EAGAIN;

        if(wait_event_interruptible(inq, !kfifo_is_empty(&tempval_fifo)))
            return -ERESTARTSYS;

        if(down_interruptible(&sem))
            return -ERESTARTSYS;
    }

    /* Ok, we have data. Transfer it to the user */
    retval = kfifo_to_user(&tempval_fifo, buf, count, &copied);

    up(&sem);

    return retval ? retval : copied;                            

}
/*
 * If our file is opened through the /dev/poll mechanism, 
 * we only update the readable mask whenever there is data 
 * available in the kfifo. The poll_wait will wait until
 * there is data in the fifo, or when inq is signalled.
 */
static unsigned int fifo_poll(struct file * file, poll_table * wait)
{
    unsigned int mask = 0;

    down(&sem);
    poll_wait(file, &inq, wait);
    if(!kfifo_is_empty(&tempval_fifo))
        mask |= POLLIN | POLLRDNORM;
    up(&sem);
    return mask;
}

/**
 * The IOCTL calls only handle one command - to set the 
 * altimeter to barometer/altimeter mode. 
 **/

/**
 * FIFO operations of our char device driver
 **/
static const struct file_operations fops = {
    .owner = THIS_MODULE,
    .read = fifo_read,
    .open = fifo_open,
    .poll = fifo_poll,
 //   .ioctl = fifo_ioctl,
    .llseek = noop_llseek,
};



/** 
 * Our ISR function. Since this is called from a 
 * threaded irq, we can safely communicate with the 
 * slower I2C bus.
 *
 * The irq function reads the data from our sensors,
 * format the returned values and push the values
 * to the kfifo.
 **/
static irqreturn_t gpio17_isr(int irq, void * dev_id)
{
    unsigned char buff[100];
    uint32_t temp_value, alt_value, len;

    struct mpl3115_data * data = dev_id;
    struct i2c_client * client = data->client;

    // First check if the data is coming from DYRDY
    if (is_data_ready_set(client))
    {
        temp_value = read_temp(client);
        alt_value = read_altbar(client);

        format_altbar_value(data->isaltmode,
            alt_value,
            data->last_altbar_value,
            STR_VAL_SIZE);

        format_temperature_value(
            temp_value,
            data->last_temp_value,
            STR_VAL_SIZE);

        len = sprintf(buff, "%s|%s\n", 
            data->last_temp_value,
            data->last_altbar_value
            );

        if (down_interruptible(&sem))
            return -ERESTARTSYS;
    
        // continue putting data, disregard if fifo is full
        kfifo_in(&tempval_fifo, buff, len);

        // release mutex/semaphore
        up(&sem);

        // Wake up readers
        wake_up_interruptible(&inq);
    }

    return IRQ_HANDLED;

}


/** 
 * The following functions are callback functions of our driver. 
 * Upon successful detection of kernel (via the mpl3115_i2c_detect 
 * function below). The kernel calls the mpl3115_i2c_probe(), the 
 * driver's duty here is to allocate the client's data, initialize
 * the data structures needed, and to call initialize the sensor
 * init(client, data->isalmode) will initialize our hardware. 
 *
 * This function also creates our char device driver and create
 * a device file in /dev/mpl3115. Once the char driver is registerd
 * it will then register an interrupt handler, the interrupt source
 * is coming from GPIO pin #17, that too is allocated and registered.
 *
 * Towards the end of this function, the device files and sysfs 
 * entries are registered.
 **/
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

    /* initialize our hardware */
    data->isaltmode = false;
    dev_info(&client->dev, "Initializing device with default barometer mode\n");
    init(client, data->isaltmode);

    /* This section creates the char device */
    mpl3115_major = register_chrdev(0, MPL3115_DEVICE_NAME,
        &fops);
    if (mpl3115_major < 0)
    {
        error = mpl3115_major;
        dev_err(&client->dev, "Error registering char dev\n");
        goto error_freemem;
    }

    mpl3115_device_class = class_create(THIS_MODULE,
        MPL3115_DEVICE_NAME);
    if(IS_ERR(mpl3115_device_class))
    {
        error = PTR_ERR(mpl3115_device_class);
        dev_err(&client->dev, "Error registering device class\n");
        goto error_unregchrdev;
    }

    mpl3115_device = device_create(mpl3115_device_class,
        NULL,
        MKDEV(mpl3115_major, 0),
        NULL,
        MPL3115_DEVICE_NAME);
    if(IS_ERR(mpl3115_device))
    {
        error = PTR_ERR(mpl3115_device);
        dev_err(&client->dev, "Error creating device file\n");
        goto error_unregclass;
    }
    dev_info(&client->dev, "Device created with major [%d]\n", mpl3115_major);

    /* Initialize the kfifo used */
    if (kfifo_alloc(&tempval_fifo, FIFO_SIZE, GFP_KERNEL))
    {
        dev_err(&client->dev,"Can not allocate kernel fifo!\n");
        error = -ENOMEM;
        goto error_freegpio;
    }
    /* Initialize the wait queue and semaphore */
    init_waitqueue_head(&inq);

    /* Initialize the semaphore used */
    sema_init(&sem,1);
    dev_info(&client->dev, "Initialize kfifo\n");

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

    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_freekfifo;
    }

    dev_info(&client->dev, "Creating sys entries\n");
    // We now register our sysfs attributs. 
    device_create_file(dev, &dev_attr_altbarmode);
    device_create_file(dev, &dev_attr_steptime);
    device_create_file(dev, &dev_attr_altbarvalue);
    device_create_file(dev, &dev_attr_tempvalue);

    return 0;

error_freekfifo:
    kfifo_free(&tempval_fifo);
error_freegpio:
    gpio_free(GPIO_INT_GPIO17);
error_unregclass:
    class_unregister(mpl3115_device_class);
    class_destroy(mpl3115_device_class);
error_unregchrdev:
    unregister_chrdev(mpl3115_major, MPL3115_DEVICE_NAME);
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, unregister
 * our char device, disconnect the irqs and free data
 * that has been allocated by our driver.
 **/
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__);

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


    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);


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

    // Free the kfifo entry
    dev_info(&client->dev, "freeing kfifo\n");
    kfifo_free(&tempval_fifo);

    // Remove char dev
    dev_info(&client->dev, "unregistering char dev\n");
    device_destroy(mpl3115_device_class, MKDEV(mpl3115_major, 0));
    class_unregister(mpl3115_device_class);
    class_destroy(mpl3115_device_class);
    unregister_chrdev(mpl3115_major, MPL3115_DEVICE_NAME);

    // 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 0x60, there is really nothing to detect.
 *
 * For validity, the device also checks the signature
 * register by calling is_valid_device().
 **/
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,
};


module_i2c_driver(mpl3115_driver);

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

Alternatively you can also get the whole project from git here.

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.

Sunday, June 8, 2014

Implementing a device driver for the MPL3115a on the Raspberry Pi

This is a quick device driver for the raspberry pi I made to experiment with the XTRINSIC MEMS Sensor board that contains 3 sensors - A temperature and barometer sensor (using the MPL3115A2 chip), a 3-axis accelerometer, and the digital compass sensor.


I'm currently interested to get the temperature and barometric pressure data for the moment, I would have to write the other drivers later. So this driver is named mpl3115 from the temperature/barometer sensor on the board. Note that for the altimeter/barometer sensor can only work in single mode - either as a barometer or an altimeter, not both. To switch modes, we would have to change the contents of the config registers of the MPL3115 to work. A kernel attribute is provided so that the altimeter/barometer mode can be changed.

Also note that the device driver implementation does not use the 10bit I2C addressing, and I made no effort to touch the repeated start feature of the device. All access to the device is done via i2c smbus read/write byte operations.

There was already a working user mode device driver for the MPL3115, but as you know user mode device drivers can not handle hardware interrupts, thus the rationale for creating this device driver. The device driver works by handling the interrupt lines (GPIO17 on the raspberry pi header) from the sensor and reading them only when there is available data (sensor raises data ready interrupt).

The driver exposes 3 kernel attributes to userland in /sys. Once the driver is loaded, it exposes the following attributes on /sys/bus/i2c/drivers/mpl3115/1-0060 directory:

altbarmode - The current mode of the driver whether it uses Barometric pressure (B) measurement or Altitude (A). By default the driver starts with Barometric pressure mode.
altbarvalue - The last update time from the driver (time barometer/alti- meter was updated) and the altimeter/barometric pressure value, depending on the mode. The format of this is given in the example below:
<hours:min:sec:micro sec>|<altimeter/barometer value>

Example below shows how readings are taken and to switch modes:
pi@raspberrypi:~$ cat /sys/bus/i2c/drivers/mpl3115/1-0060/altbarmode
B
pi@raspberrypi:~$ cat /sys/bus/i2c/drivers/mpl3115/1-0060/altbarvalue
10:39:8:107541|6426032
pi@raspberrypi:~$ echo 'A' > /sys/bus/i2c/drivers/mpl3115/1-0060/altbarmode
pi@raspberrypi:~$ cat /sys/bus/i2c/drivers/mpl3115/1-0060/altbarvalue
10:39:49:688672|19632
pi@raspberrypi:~$
Notice the difference in reading altimeter mode and barometer mode above. Also note that the value is the raw readings of the sensor, user applications must convert this value to meters/feet/etc.
tempvalue - The value of the temperature sensor. This value is a 16 bit value containing the numbers part in the MSB, and the decimal part in the LSB. Since we are prohibited from using floating point in kernel code, user applications must convert this value to Fahrenheit using the formula:
   
    double fahrenheit = 0.0;
    double MSB = (value >> 8) & 0xFF;
    double LSB = value & 0xFF;

    LSB = (LSB > 99) (LSB / 1000) : (LSB / 100);

    fahrenheit = MSB + LSB;
So for the readings below when examining the value of the tempvalue:

pi@raspberrypi:~$ cat /sys/bus/i2c/drivers/mpl3115/1-0060/tempvalue
10:57:2:914445|7856
pi@raspberrypi:~$
7856 is 0x1EB0 (MSB = 1E or 30, LSB = B0 or ), to get the reading in fahrenheit
C = 30.176 ( That's how hot Singapore is! )
If you put your finger on the MPL3115 chip, notice the temperature increase.
Right now only the last read values are taken by this driver, theres currently no kernel FIFO to store all the values when an interrupt is triggered, what was last ready will be reflected. In the future I will expand this driver to store all read values from the sensor in a kernel fifo (or circular buffer) and implements a char driver.

Source code for this driver is in github - https://github.com/vpcola/drivers/tree/master/mpl3115a

Update: Tried to review the code last night and found a bunch of bugs with the code. The values can't be right. All along I thought that the MSB in the temperature readings were in degrees Fahrenheit, but its not the case - its actually in Celsius. There was also a bug in calculating the decimal part of the temperature. I was looking at the Python implementation that was included in the distribution of the Xtrinsic board and found this:

        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)

However the official specs of the mpl3115a states that:

This means there is a bug in the python code in the original release.

So the decimal part is actually in steps of 256. That means whatever value in the LSB, you would need to divide this by 256 to get the decimal value part of the temperature. I have corrected the previous driver and added a KFIFO that can be accessed using the /proc directory. This is detailed in my next blog.

Saturday, June 7, 2014

ChibiOS/RT, I2C on the Raspberry Pi Rev. 2 board

As an alternative to loading Linux on the Raspberry Pi, you maybe looking for a real-time kernel with a small footprint where you can run your real-time applications. A real-time os might be appropriate for applications that need low latency and real-time response to events. By real-time response, I meant predictable response - a non-real time OS doesn't guarantee that. There were a variety of real-time kernels out there including FreeRTOS and ChibiOS/RT, the later being one of my favorites.

There was already a very good starting point where ChibiOS/RT is ported to the raspberry pi:

http://www.stevebate.net/chibios-rpi/GettingStarted.html

However, Steve's implementation uses a raspberry pi that is older than the version that I have. The older version of the raspberry pi is using i2c bus 0 on the header, the raspberry pi revision 2 boards uses i2c bus 1 instead. And so because of this, we need to make some revisions to the initial port.

We would also need an appropriate cross compiler tool chain that targets ARM bare metal binaries. I am on Fedora 19, the cross compiler tool chain that we are going to use will be from the Fedora distribution itself. There's another Linaro toolchain that I could use, but opted to use Fedora instead since its readily available. To download the tool chain, you could use yum:


[root@vcodev vco]# yum install arm-none-eabi*

That should download all the tool necessary for this experiment.

Once we have the tools necessary, you should be able to follow the steps below:

1) Clone steve's chibios sources


[vco@vcodev Projects]$ git clone https://github.com/steve-bate/ChibiOS-RPi
Cloning into 'ChibiOS-RPi'...
remote: Counting objects: 56983, done.
remote: Compressing objects: 100% (10494/10494), done.
remote: Total 56983 (delta 45384), reused 56983 (delta 45384)
Receiving objects: 100% (56983/56983), 29.76 MiB | 1.55 MiB/s, done.
Resolving deltas: 100% (45384/45384), done.
Checking connectivity... done.
[vco@vcodev Projects]$

2) Once downloaded, navigate into the HAL drivers directory. This contains hardware device drivers for the BCM2835 chip (raspberry pi).


[vco@vcodev Projects]$ cd ChibiOS-RPi/os/hal/platforms/BCM2835/
[vco@vcodev BCM2835]$

3) We will then make changes to three files i2c_lld.c, i2c_lld.h, and hal_lld.c. The changes that we are going to do is to make the I2C bus 1 (in raspberry pi revision 2 board) available in the headers. For convenience, I've provided a patch file so you won't have to edit the sources manually. The patch file is located here

You can apply the patch with


[vco@vcodev ChibiOS-RPi]$ patch -p0 < patchfile
patching file os/hal/platforms/BCM2835/hal_lld.c
patching file os/hal/platforms/BCM2835/i2c_lld.c
patching file os/hal/platforms/BCM2835/i2c_lld.h
[vco@vcodev ChibiOS-RPi]$

To get an idea of what has been changed, here's a summary of changes from the i2c_lld.c file.


 38 /*===========================================================================*/
 39 /* Driver exported variables.                                                */
 40 /*===========================================================================*/
 41
 42 I2CDriver I2C0;
 43 I2CDriver I2C1;

Line 43 above simply diclares I2C1 driver for bus 1. 


136 /**
137  * @brief   Low level I2C driver initialization.
138  *
139  * @notapi
140  */
141 void i2c_lld_init(void) {
142   I2C0.device = BSC0_ADDR;
143   i2cObjectInit(&I2C0);
144
145   I2C1.device = BSC1_ADDR;
146   i2cObjectInit(&I2C1);
147 }

Lines 145 and 146 initializes the bus driver.


149 /**
150  * @brief   Configures and activates the I2C peripheral.
151  *
152  * @param[in] i2cp      pointer to the @p I2CDriver object
153  *
154  * @notapi
155  */
156 void i2c_lld_start(I2CDriver *i2cp) {
157   /* Set up GPIO pins for I2C */
158   bcm2835_gpio_fnsel(GPIO0_PAD, GPFN_ALT0);
159   bcm2835_gpio_fnsel(GPIO1_PAD, GPFN_ALT0);
160
161   /* Set up GPIO pins for I2C on Rev. 2 boards*/
162   bcm2835_gpio_fnsel(GPIO2_PAD, GPFN_ALT0);
163   bcm2835_gpio_fnsel(GPIO3_PAD, GPFN_ALT0);

Lines 162 and 163 selects an alternate function for I2C on the raspberry pi header. Based on the specs for revision 2, GPIO2 and GPIO3 are now exported on the header instead of GPIO0 and GPIO1.


4) Now that our ChibiOS sources are patched. We now create a ChibiOS project where we can use the i2c bus for our experiments. For this example, we will be writing to the I2C bus - to device address 0x21 which holds our LED's via the MCP23017 I2C IO extender chip.

5) To get around this quickly, we will simply have to copy an existing example and modify it rather than having to build it from scratch. Navigate to the ChibiOS-RPi/demos directory. 

There's an existing ARM11-BCM2835-GCC project that we can use as a template for this experiment. You may want to copy this to a new directory or edit the project in place. This project already enables I2C on the build (see halconf.h), so we don't have to make necessary adjustments. The only thing we need to do is to define a thread in ChibiOS that will continually write a value to the I2C bus (in this case a counter value).

6) Edit the file main.c and add the following lines before the beginning of the main() function.


static const uint8_t i2cled_address = 0x21;
static const uint8_t i2cled_writereg = 0x14;
static const uint8_t i2cled_confreg = 0x00;

static msg_t i2cled_write(uint8_t device_address,
        uint8_t register_address,
        uint8_t data)
{
    uint8_t request[2];
    request[0] = register_address;
    request[1] = data;

    i2cAcquireBus(&I2C1);
    msg_t status = i2cMasterTransmitTimeout(
            &I2C1, device_address, request, 2,
            NULL, 0, MS2ST(1000));
    i2cReleaseBus(&I2C1);

    if (status != RDY_OK)
        chprintf((BaseSequentialStream *)&SD1, "Error while writing to i2cled: %d\r\n", status);

    return status;
}

static void i2cled_init(uint8_t device_address, uint8_t dirmask)
{
    msg_t status = i2cled_write(device_address,
            i2cled_confreg, // direction register.
            dirmask);

    if (status != RDY_OK)
        chprintf((BaseSequentialStream *)&SD1, "Error while setting direction mask: %d\r\n", status);
}

static WORKING_AREA(waI2CCounterThread, 128);
static msg_t I2CCounterThread(void *p)
{
    (void)p;
    chRegSetThreadName("counter");
    i2cled_init(i2cled_address, 0x00);
    uint8_t count = 0;
    while (TRUE)
    {
        i2cled_write(i2cled_address, i2cled_writereg, count);
        chThdSleepMilliseconds(100);
        count++;
    }

    return 0;
}

7) Now at the main function, add the following lines below before the event servicing loop of the chibios rtos begins:


  /*
   * I2C Initialization
   */
  I2CConfig i2cConfig;
  i2cStart(&I2C1, &i2cConfig);

  /*
   * Create the i2c led counter thread
   */
  chThdCreateStatic(waI2CCounterThread, sizeof(waI2CCounterThread), NORMALPRIO, I2CCounterThread, NULL);

8) Now that we have created the counter thread, we can go ahead and compile our chibios, to do that simply invoke make from the demos/ARM11-BCM2835-GCC directory:


[vco@vcodev ARM11-BCM2835-GCC]$ make
Compiler Options
arm-none-eabi-gcc -c -mcpu=arm1176jz-s -O2 -ggdb -fomit-frame-pointer -ffunction-sections -fdata-sections -Wall -Wextra -Wstrict-prototypes -Wa,-alms=build/lst/ -mno-thumb-interwork -MD -MP -MF .dep/build.d -I. -I../../os/ports/GCC/ARM -I../../os/ports/GCC/ARM/BCM2835 -I../../os/kernel/include -I../../test -I../../os/hal/include -I../../os/hal/platforms/BCM2835 -I../../boards/RASPBERRYPI_MODB -I../../os/various main.c -o main.o

mkdir -p build/obj
mkdir -p build/lst
Compiling ../../os/ports/GCC/ARM/crt0.s
Compiling ../../os/ports/GCC/ARM/chcoreasm.s
Compiling ../../os/ports/GCC/ARM/BCM2835/vectors.s
Compiling ../../os/ports/GCC/ARM/chcore.c
Compiling ../../os/kernel/src/chsys.c
Compiling ../../os/kernel/src/chdebug.c
Compiling ../../os/kernel/src/chlists.c
Compiling ../../os/kernel/src/chvt.c
Compiling ../../os/kernel/src/chschd.c
Compiling ../../os/kernel/src/chthreads.c
Compiling ../../os/kernel/src/chdynamic.c
Compiling ../../os/kernel/src/chregistry.c
Compiling ../../os/kernel/src/chsem.c
Compiling ../../os/kernel/src/chmtx.c
Compiling ../../os/kernel/src/chcond.c
Compiling ../../os/kernel/src/chevents.c
Compiling ../../os/kernel/src/chmsg.c
Compiling ../../os/kernel/src/chmboxes.c
Compiling ../../os/kernel/src/chqueues.c
Compiling ../../os/kernel/src/chmemcore.c
Compiling ../../os/kernel/src/chheap.c
Compiling ../../os/kernel/src/chmempools.c
Compiling ../../test/test.c
Compiling ../../test/testthd.c
Compiling ../../test/testsem.c
Compiling ../../test/testmtx.c
Compiling ../../test/testmsg.c
Compiling ../../test/testmbox.c
Compiling ../../test/testevt.c
Compiling ../../test/testheap.c
Compiling ../../test/testpools.c
Compiling ../../test/testdyn.c
Compiling ../../test/testqueues.c
Compiling ../../test/testbmk.c
Compiling ../../os/hal/src/hal.c
Compiling ../../os/hal/src/adc.c
Compiling ../../os/hal/src/can.c
Compiling ../../os/hal/src/ext.c
Compiling ../../os/hal/src/gpt.c
Compiling ../../os/hal/src/i2c.c
Compiling ../../os/hal/src/icu.c
Compiling ../../os/hal/src/mac.c
Compiling ../../os/hal/src/mmc_spi.c
Compiling ../../os/hal/src/mmcsd.c
Compiling ../../os/hal/src/pal.c
Compiling ../../os/hal/src/pwm.c
Compiling ../../os/hal/src/rtc.c
Compiling ../../os/hal/src/sdc.c
Compiling ../../os/hal/src/serial.c
Compiling ../../os/hal/src/serial_usb.c
Compiling ../../os/hal/src/spi.c
Compiling ../../os/hal/src/tm.c
Compiling ../../os/hal/src/uart.c
Compiling ../../os/hal/src/usb.c
Compiling ../../os/hal/platforms/BCM2835/hal_lld.c
Compiling ../../os/hal/platforms/BCM2835/pal_lld.c
Compiling ../../os/hal/platforms/BCM2835/serial_lld.c
Compiling ../../os/hal/platforms/BCM2835/i2c_lld.c
Compiling ../../os/hal/platforms/BCM2835/spi_lld.c
Compiling ../../os/hal/platforms/BCM2835/gpt_lld.c
Compiling ../../os/hal/platforms/BCM2835/pwm_lld.c
Compiling ../../os/hal/platforms/BCM2835/bcm2835.c
Compiling ../../boards/RASPBERRYPI_MODB/board.c
Compiling ../../os/various/shell.c
Compiling ../../os/various/chprintf.c
Compiling main.c
Linking build/ch.elf
Creating build/ch.hex
Creating build/ch.bin
Creating build/ch.dmp
Done
[vco@vcodev ARM11-BCM2835-GCC2]$

9) After compiling, the os image is now stored in build/ch.bin. Remember in the Raspberry pi, the kernel image is stored in kernel.img on your SD card. To boot chibios, simply replace the kernel.img on your SD card with ch.bin (in my case the sd card's fat partition is mounted to /media/chibifat directory):



[vco@vcodev ARM11-BCM2835-GCC2]$ cd build
[vco@vcodev build]$ ls
ch.bin  ch.dmp  ch.elf  ch.hex  ch.map  lst  obj
[vco@vcodev build]$ cp ch.bin /media/chibifat/kernel.img

10) Now we have a bootable chibios image that we can run on our raspberry pi. The ChibiOS for raspberry pi also includes a shell which is available via the UART on the header. If you happen to have the FTDI pi to TTL adapter, go ahead and connect those, as they'll become important during debugging. Take note that the raw RX and TX pins on the raspberry pi breakout header are NOT TTL 5V, connecting it directly to your comm port may fry your raspberry pi board. 




The only downside on ChibiOS/RT on the raspberry pi is that the RTOS might be over dressed for such hardware. Note that ChibiOS/RT doesn't yet support USB host though, it does have USB device and CDC. But the most important hardware on the raspberry pi that I would really love an ChibiOS to access is ethernet. Unfortunately the ethernet hardware on the raspberry pi is implemented on top of the USB host controller chip - something that ChibiOS/RT does not support. I guess I'll just have to wait ...

Using Raspberry Pi's GPIO as an interrupt source III

In my previous experiments using the GPIO as our interrupt source, I merely respond to interrupts by printing a kernel message whenever the tasklet is executed.

On this experiment, I'll explore another way of handling hardware interrupts by using a threaded irq. This threaded irq isr is different from a normal isr, this now allows us to communicate with much slower hardware such as the ones connected via the i2c bus. 

You can get the source code for this here.

I've stripped down the original chip_i2c driver to provide only sysfs attributes (chip_led and chip_switch), it does not anymore provide the char device functionality. I also removed some mutex (as mutex are bad when used from within an isr context), instead I rely mostly on the assumption that there is only one device that maybe controlling the smbus (master) at any one point of time - I maybe wrong here, so please hit comments.

Again to get the module working, we need to add the i2c board info for this in the board setup routine described in my previous blog. I2C doesn't support enumeration the likes of usb and pci, so the kernel needs to know about the device beforehand, otherwise the device won't be loaded into the kernel. Details on the board setup and the original chip_i2c driver is here.



Thursday, June 5, 2014

I2C, Nucleo STM32F401RE and FreeRTOS

My Nucleo board arrived today, I have been eagerly awaiting its arrival since I purchased it last week. The board comes with a powerful ARM Cortex M4 processor with max speed of 84MHz. This should be enough for limited DSP functionality (or better get an ARM Cortex M4F instead). This morning I've decoupled my previous breadboard (MCP23017 with leds) setup to test the I2C bus on this new board. My aim was just to familiarize myself with the board, use the tools that come along with it and develop a simple application to write to the I2C bus.

The nucleo board also comes with an arduino compatible header which is great since I also happen to have a lot of arduino sheilds. Do note that the nucleo board uses 3.3 volt logic and it may not be compatible with other arduino shields. To give an overview of this experiment, here's my running setup:


So it's a very simple setup, we output an 8 bit counter value to the leds and loop.

To accomplish this experiment, we need the following tools:

- Download STM32CubeMX from STMicroelectronics here. You will find the link to the download at the end of the page.  We will use the CubeMX software to generate an initialization code for our nucleo F401RE board. It will also generate a template project for your compiler tool chain. There is also an online ide at mbed, but I currently do not use it since I work offline most of the time.

- I currently own an Embedded Workbench for ARM tool chain, technically the CubeMX software can generate targets for other tool chain. EWARM is the one I currently have for now.

- Install the needed Nucleo drivers at this link. And while you are on that page, download also the firmware update to fix some important issues with the board.

- The MCP23017 (I2C io extender) setup and schematics are discussed in my previous blog, maybe you should examine it first.

Once everything is downloaded and set-up, it's time to have some fun with the board. 

1) Let us try to setup our board initialization with CubeMX to generate our project files. Open CubeMX software and click on "New Project".

2) Once done, you will be prompted to select the board setup similar to the image shown below. Select the board selector tab and choose the Nucleo F401RE board.




3) After the board has been selected, the default configuration scree appears. This allows us to choose the pin assignments for the board. 



4) On the left side notice that there are currently no I2C buses that are enabled, choose I2C1 (expand it), and select "I2C" instead of disabled. FreeRTOS is not necessary, but since I'd be doing experiments with it in the future, we go ahead and enable it now (expand and check the enabled check box).

5) The default I2C lines on the Nucleo board are assigned to PB6/PB7. I need to re-assign these so that it would be easier to attach it to the headers. Looking at the board specs, the alternate configuration for it is PB8/PB9 which is mapped to the arduino CN5 header. To do this, Ctrl-Click on the original PB6/PB7 and notice that the alternate ports (highlighted) are shown on the CubeMX gui. To re-assign, simply right click on the alternate port and click "I2C1_CLK" or "I2C1_SDA" for PB8 and PB9. Your setup should be exactly like the image above (where ports are already re-assigned).

6) Next we need to setup our clock, select the "Clock Configuration" tab. The default configuration for CubeMX assumes that our board will be powered by an external oscillator. But this is not the case, there's no crystal assigned to C33 and C34 on the nucleo board, so our clock source will entirely be from the internal clock oscillator. To configure it, configure your setup similar to the setup below:


Note that in the above setup, I choose the internal oscillator (as source to the PLL) to provide the 84Mhz clock to our system.

7) Before we generate the project, we need to review the current configuration. Go ahead and click the configuration tab. Notice the settings that were enabled.

8) Another thing worth noting is the Nested Vector interrupt configuration (NVIC), click on the NVIC button while on the configuration screen and you should be taken to the screen shown below:


This screen allows us to configure interrupts and their priority. Right now leave this configuration as it is (only system tick timer is enabled).

9) We can now go ahead and generate the project files. Do this by saving the project first (diskette icon), the pop-up dialog will prompt you for the location where you want the project to be stored. 

10) Finally generate the project files via the (gear icon) generate button. The pop-up dialog will prompt you for the location where the project will be stored and the type of project files that will be generated. In my case, this would be EWARM (Embedded Workbench). 

11) Open the project in embedded workbench, we'll first try to examine the settings that are generated by CubeMX and make necessary adjustments.




12) In the project window, click on the generated stm32f4xx_hal_msp.c file. This file contains callbacks used by the HAL to initialize/setup user defined ports. Navigate into the HAL_I2C_MspInit() function:

void HAL_I2C_MspInit(I2C_HandleTypeDef* hi2c)
{

  GPIO_InitTypeDef GPIO_InitStruct;
  if(hi2c->Instance==I2C1)
  {
    /* Peripheral clock enable */
    __I2C1_CLK_ENABLE();
  
    /**I2C1 GPIO Configuration    
    PB8     ------> I2C1_SCL
    PB9     ------> I2C1_SDA 
    */
    GPIO_InitStruct.Pin = GPIO_PIN_8|GPIO_PIN_9;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_OD;
    GPIO_InitStruct.Pull = GPIO_PULLUP;
    GPIO_InitStruct.Speed = GPIO_SPEED_HIGH;
    GPIO_InitStruct.Alternate = GPIO_AF4_I2C1;
    HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);

  }

In the above code, change the GPIO_InitStruct.Pull to GPIO_PULLUP, we need a pullup resistor to drive both lines of the I2C bus. We also need to change the speed to GPIO_SPEED_HIGH.

13) In the main.c file, we need to create a user thread that would count and output the count value to our MCP23017 chip at address 0x21. First create a function prototype at the beginning of the main.c file:

static void StartThread(void const * argument);
static void CounterThread(void const * argumnet); /* For our led counter */

We then define the function, insert the line of code below right after the StartThread function:

static void CounterThread(void const * argument) {

  /* USER CODE BEGIN 5 */
  uint8_t count = 0;
  char data[2];
  
  /* We need to send data to the direction 
   * register first to setup our led counter as output
   */
  data[0] = 0x00;
  data[1] = 0x00;
  HAL_I2C_Master_Transmit(&hi2c1, (uint16_t) (0x21 << 1), (uint8_t *) data, 2, 1000);
 
  /* Infinite loop */
  data[0] = 0x14;
  for(;;)
  {
    data[1] = count;
    HAL_I2C_Master_Transmit(&hi2c1, (uint16_t) (0x21 << 1), (uint8_t*) data, 2, 1000);
    count ++;
    osDelay(100);
  }
  /* USER CODE END 5 */ 
}

The function HAL_I2C_Master_Transmit() function sends the data to the I2C bus, it takes the handle to the i2c bus (hi2c1), the device address - shifted one bit to the left since we use 7 bit addressing mode, the data to write and the size of the data to write. The last parameter is the timeout value in ms (in this case, I use 1000).

14) We then register our thread in FreeRTOS, in the main() function, add the following lines of code before the scheduler is started (otherwise our thread will never be executed).

  /* Code generated for FreeRTOS */
  /* Create Start thread */
  osThreadDef(USER_Thread, StartThread, osPriorityNormal, 0, 2 * configMINIMAL_STACK_SIZE);
  osThreadCreate (osThread(USER_Thread), NULL);

  /* Create Count led thread */
  osThreadDef(USER_Thread1, CounterThread, osPriorityNormal, 0, 2 * configMINIMAL_STACK_SIZE);
  osThreadCreate (osThread(USER_Thread1), NULL);
  
  /* Start scheduler */
  osKernelStart(NULL, NULL);


osThreadDef() defines our thread taking into its arguments the name of our thread function (CounterThread), the thread priority and the name of the thread handle (USER_Thread1).

15) Compile the project and check for errors. You can right click on Embedded Workbench's project and click "Build All".

16) Before we could transfer or debug our design on the Nucleo board, we need to configure our debugging options. Right click on the project and select "Options" :


Make sure on the ST-Link category, SWD is selected (unless you have the expensive JTAG cable) and set the CPU clock frequency.

17) You should now be able to debug and download the file to the Nucleo board.