How to write a driver for the Thingsquare SDK

This document will go through writing a driver for a device running the Thingsquare system, by example.

Drivers and the requirements on such vary vastly, and there will always be cases that will not be covered by this document. However, many drivers follow common patterns. Below we will go through two cases that can be seen as generalized cases. Many drivers look like these at large, although it might be SPI instead of I2C, there may be more routines involved in eg calibration, configuration, handling power consumption and sleep, converting data (eg from raw samples to millivolts), averaging data, much more error handling, and so on.

The driver examples we are looking at are as follows.

  • A very simple driver - no long-running operations, no interrupts, no complex hardware peripherals involved
  • A medium complexity driver - operations make take a long time, we have interrupts and i2c communication

Let's call them the simple and medium drivers for short.

Bear in mind that these examples are examples. There are many ways these can be implemented, and what suits the sensor you have at hand might not at all be a fit with any of these examples.

Downloads

The example drivers and the application are available as a download from here. Feel free to use them as a template.

Simple driver

This simple example driver will control a GPIO pin. While it may seem silly to write an entire driver for simple control of a GPIO - in most cases setting or clearing the pin is a one-liner - containing this in a driver will allow us to have better modularity code-wise, allow room for growing and hiding platform-specific things, and makes the purpose of the pin crystal clear since the naming of the driver reflects this.

In this example, we will use a GPIO pin to control a power supply. Setting the pin (ie putting it in a logic high state, 1) will turn on an external power supply, while clearing it (logic low state, 0) will turn off the power supply. This is not uncommon for eg LED high-power light drivers, or motor controllers.

First, we name the driver and create the files. Here, let's create power-control.c and power-control.h.

The driver will need three functions: initialization, power on, and power off. Let's write definitions for that in the header file:

#ifndef POWER_CONTROL_H
#define POWER_CONTROL_H
/*---------------------------------------------------------------------------*/
#include "thsq.h"
#include "ti-lib.h"
/*---------------------------------------------------------------------------*/
/*
 * Initialize the power control driver.
 *   pin  is the pin number of the GPIO used for power control
 *        valid range IOID_0 to IOID_30, and IOID_UNUSED.
 *        caller is responsible for chosing a pin that is not already in use
 *        by other drivers.
 * returns <0 for fail, 0 for success
 */
int power_control_init(int pin);

/* Turn on the power supply; returns <0 for fail, 0 for success */
int power_control_on(void);

/* Turn off the power supply; returns <0 for fail, 0 for success */
int power_control_off(void);
/*---------------------------------------------------------------------------*/
#endif  /* POWER_CONTROL_H */

Then, the code file will get the actual functions. Note that this driver is for the CC1310/CC1350/CC2650 chips. For CC2538, we would need a different definition of the pin argument.

#include "thsq.h"
#include "power-control.h"
#include "ti-lib.h"
/*---------------------------------------------------------------------------*/
static int psu_pin = IOID_UNUSED;
/*---------------------------------------------------------------------------*/
int
power_control_init(int pin)
{
  if((pin < IOID_0 || pin > IOID_30) && pin != IOID_UNUSED) {
    return -1;
  }

  /* init pin; default to OFF */
  psu_pin = pin;
  ti_lib_ioc_pin_type_gpio_output(psu_pin);
  power_control_off();

  return 0;
}
/*---------------------------------------------------------------------------*/
int
power_control_on(void)
{
  if(psu_pin != IOID_UNUSED) {
    ti_lib_gpio_write_dio(psu_pin, 1);
    return 0;
  }
  return -1;
}
/*---------------------------------------------------------------------------*/
int
power_control_off(void)
{
  if(psu_pin != IOID_UNUSED) {
    ti_lib_gpio_write_dio(psu_pin, 0);
    return 0;
  }
  return -1;
}
/*---------------------------------------------------------------------------*/

All functions are short, they do not involve interrupts, and have no side-effects other than on this single GPIO pin. Later on, we could expand this driver with whatever functionality might be needed, such as timestamping, or keeping track of for how long time the power supply has been in "on"-mode, or even replacing the entire way the power supply is controlled with another way (eg SPI instead of GPIO). It also makes version control much simpler.

Here is an example application that uses this driver, and the corresponding change to the project Makefile necessary to compile the driver. The application checks for the psu variable and sets the power supply control accordingly. Note that this is of course a simplified example. Also note that the driver itself could have a Thingsquare callback and follow the psu variable accordingly.

#include "thsq.h"
#include "power-control.h"
/*---------------------------------------------------------------------------*/
static void
callback_thsq(enum thsq_reason r, const char *str, int len)
{
  if(r == THSQ_KEYVAL) {
    if(strncmp(str, "psu", 3) == 0 && thsq_exists("psu")) {
      /* PSU variable updated; control PSU accordingly */
      if(thsq_get("psu") > 0) {
        power_control_on();
      } else {
        power_control_off();
      }
    }
  }
}
/*---------------------------------------------------------------------------*/
void
app(void)
{
  /* init the PSU power control driver */
  power_control_init(IOID_23);
  power_control_on();

  /* Set up the callback */
  static struct thsq_callback cb;
  thsq_add_callback(&cb, callback_thsq);
}
/*---------------------------------------------------------------------------*/

The project Makefile needs a change in order to compile the driver. The following shows the change - add the line SOURCEFILES += and add the .c-files that needs to be compiled, one by one with a space between. To add more files, simply write them after eachother, like this: SOURCEFILES += power-control.c abc123.c.

# Add the PSU power control driver to the compilation.
SOURCEFILES += power-control.c

Medium driver

This medium driver example will involve a slow sensor, such as an environmental sensor sampling the barometric pressure. This hypothetical sensor communicates over I2C and spends most of the time in deep sleep. We sample the sensor by waking it up and trigger a sample, then we wait, and finally read the sample data out. Let's also say that the amount of time necessary for a sample to finish depends on a number of factors out of our control. We therefore don't know exactly when the sample will be ready. The sensor will notify the CPU when it is done through the use of an interrupt pin. The interrupt pin will transition from low state to high state when done.

Let's pretend the the sensor chip is called ABC123 and start writing the driver. For brevity, we'll leave out the i2c routines.

The header file, abc123.h, might look like this.

#ifndef ABC123_H
#define ABC123_H
/*---------------------------------------------------------------------------*/
#include "thsq.h"
#include "ti-lib.h"
/*---------------------------------------------------------------------------*/
/*
 * Driver for the ABC123 sensor.
 * Longer description here...
 */
/*---------------------------------------------------------------------------*/
/* callback definition; will be invoked when the sensor is done sampling */
typedef void (*abc123_callback_t)(uint16_t);

/*
 * Init the sensor. Configures the sensor and leaves it in deep sleep.
 * Can only be called once. Must be called before any other function is called.
 * arguments
 *    i2c_sda      The GPIO used for I2C SDA
 *    i2c_scl      The GPIO used for I2C SCL
 *    irqpin       The GPIO used for interrupt, trigger on rising edge
 * returns
 *    -1    failed, already initialized
 *    0     success
 */
int abc123_init(int i2c_sda, int i2c_scl, int irqpin);

/*
 * Start a sensor sample. The ABC123 sensor will signal an interrupt when done.
 * The sensor sample is automatically read out and buffered, available through
 * calling abc123_get_last()
 * arguments
 *    cb      The callback that will be invoked when sample is read out
 *            the callback will be provided the sample value as argument
 * returns
 *    -1    failed, not initialized
 *    0     success
 */
int abc123_start_sample(abc123_callback_t cb);

/*
 * Get the last sample value.
 * returns
 *    0 until first sample is taken
 *    then, the last sensor sample
 */
uint16_t abc123_get_last(void);
/*---------------------------------------------------------------------------*/
#endif  /* ABC123_H */

Then, the driver code file. Of note here is the use of interrupts and the process. From the interrupt context, we may not do very much since it may interfere with other, more important tasks. Note that this interrupt may come in the middle of a transmission.

Therefore, the best course of action in the interrupt handler is to do simple tasks and tell the process to do the heavy lifting. The way we do this is through the use of process_poll(&the_process);. Using eg process_post() is not allowed in interrupt context, since it may interfere with the process scheduler. A process poll on the other hand, does not interfere and is safe to use.

Since routines for I2C (or SPI) may be long, and often depends heavily on the specific sensor at hand, those parts are left out.

#include "thsq.h"
#include "abc123.h"
#include "ti-lib.h"
#include "gpio-interrupt.h"
/*---------------------------------------------------------------------------*/
PROCESS(abc123_process, "ABC123 Sensor Process");

abc123_callback_t callback = NULL;
static int sda = IOID_UNUSED;
static int scl = IOID_UNUSED;
static int irq = IOID_UNUSED;
static uint16_t last_sample = 0;
static int inited = 0;

/* tell sensor to start sampling */
static void start_sample(void);

/* read last sample from sensor */
static uint16_t read_sample(void);

/* GPIO interrupt handler */
static void irq_handler(uint8_t ioid);

/* configuration for the button interrupt; rising edge when done */
#define IRQ_GPIO_CFG            (IOC_CURRENT_2MA  | IOC_STRENGTH_AUTO | \
                                 IOC_IOPULL_UP    | IOC_SLEW_DISABLE  | \
                                 IOC_HYST_DISABLE | IOC_RISING_EDGE   | \
                                 IOC_INT_ENABLE   | IOC_IOMODE_NORMAL | \
                                 IOC_INPUT_ENABLE)
/*---------------------------------------------------------------------------*/
int
abc123_init(int i2c_sda, int i2c_scl, int irqpin)
{
  if(inited) {
    return -1;
  }

  sda = i2c_sda;
  scl = i2c_scl;
  irq = irqpin;

  /* set up i2c */
  // left out for brevity

  /* init GPIO for irq */
  ti_lib_gpio_clear_event_dio(irq);
  ti_lib_rom_ioc_pin_type_gpio_input(irq);
  ti_lib_rom_ioc_port_configure_set(irq, IOC_PORT_GPIO, IRQ_GPIO_CFG);
  gpio_interrupt_register_handler(irq, irq_handler);
  ti_lib_rom_ioc_int_enable(irq);

  inited = 1;

  return 0;
}
/*---------------------------------------------------------------------------*/
int
abc123_start_sample(abc123_callback_t cb)
{
  if(inited == 0) {
    return -1;
  }

  process_start(&abc123_process, NULL);
  return 0;
}
/*---------------------------------------------------------------------------*/
uint16_t
abc123_get_last(void)
{
  if(inited == 0) {
    return 0;
  }

  return last_sample;
}
/*---------------------------------------------------------------------------*/
PROCESS_THREAD(abc123_process, ev, data)
{
  PROCESS_BEGIN();

  /* tell sensor to sample, then wait for it to finish */
  start_sample();
  PROCESS_WAIT_EVENT_UNTIL(ev == PROCESS_EVENT_POLL);

  /* read out and invoke callback */
  last_sample = read_sample();
  if(callback != NULL) {
    callback(last_sample);
  }

  PROCESS_END();
}
/*---------------------------------------------------------------------------*/
static uint16_t
read_sample(void)
{
  uint16_t ret;
  /* ... here we read the sensor sample over i2c */
  ret = 0;/* XXX left out for brevity */
  return ret;
}
/*---------------------------------------------------------------------------*/
static void
start_sample(void)
{
  /* ... here we command the sensor over i2c to start the sample process */
  /* XXX left out for brevity */
}
/*---------------------------------------------------------------------------*/
static void
irq_handler(uint8_t ioid)
{
  /*
   * since the irq callback can be used for more than one GPIO, we check this
   * here, although this is really not necessary since we only use it for one.
   */
  if(ioid == irq) {
    /*
     * In the interrupt context, we do the very least possible. For example,
     * we do not perform any i2c (or similar) communication. We could timestamp
     * this here if we wanted though.
     */
    process_poll(&abc123_process);
  }
}
/*---------------------------------------------------------------------------*/

And finally the application that calls the driver.

#include "thsq.h"
#include "abc123.h"
/*---------------------------------------------------------------------------*/
static void
sensor_callback(uint16_t val)
{
  /*
   * We are invoked when the abc123 sensor has sampled. This is not in an
   * interrupt context, so it's ok to set the variable and push the data.
   * For the variable name, we use a semantically better name, not "abc123".
   * Let's use "bmp" as in "barometric pressure".
   */
  thsq_sset("bmp", (int)val);
  thsq_push();
}
/*---------------------------------------------------------------------------*/
static void
callback_thsq(enum thsq_reason r, const char *str, int len)
{
  if(r == THSQ_PERIOD) {
    /*
     * Time to collect sensor data and push to the backend. We start our sensor
     * and push when done.
     */
    if(abc123_start_sample(sensor_callback) < 0) {
      /* handle error */
    }
  }
}
/*---------------------------------------------------------------------------*/
void
app(void)
{
  /* init the ABC123 sensor driver */
  if(abc123_init(IOID_28, IOID_29, IOID_30) < 0) {
    /* handle error */
  }

  /* Set up the callback */
  static struct thsq_callback cb;
  thsq_add_callback(&cb, callback_thsq);
}
/*---------------------------------------------------------------------------*/

Including files and folders in the build

When you have written a driver, it needs to be included in the compilation. As we saw above, we can do this by adding to the project Makefile,

SOURCEFILES += power-control.c

If you have many drivers, or shared drivers, it is good to store them in a common directory. A sample directory structure could like this,

projects/streetlight/
      streetlight.c
      Makefile
projects/sensor/
      sensor.c
      Makefile
projects/drivers/
      powercontrol.c
      powercontrol.h
      adxl345.c
      adxl345.h
      sht21.c
      sht21.h

To include these in the build, the Makefile eg projects/streetlight/Makefile should instead look like this,

SOURCEFILES += power-control.c adxl345.c sht21.c
SOURCEDIRS += ../drivers

To add more folders, just add more SOURCEDIRS += entries.