Ruuvi Firmware – Part 14: Accelerometer Interrupt

Ruuvi Firmware series part 14 intro image

In this part of the tutorial we’ll use interrupts to detect movement of the tag as well as to read buffered sample from the accelerometer. Final code of this blog post can be downloaded at Ruuvi GitHub in the ruuviblog-branch, tag 3.14.0-alpha.

Please follow part 1 of the series for details on how to clone the repository and compile the code. Final hex of this tutorial can be downloaded from the Ruuvi Jenkins.

Detecting Movement

There’s a lot of cases where we want to detect if RuuviTag is moving. Maybe we’re running asset tracking application and want to broadcast data faster while tag is moving, or maybe we want to detect events such as machine turning on or door being opened.

This detection could be handled in software, however our accelerometer LIS2DH12 has already the required interrupt generation so we’ll opt to using it.

Our tag experiences constant 1 G acceleration towards the center of the earth, i.e gravity. This acceleration component is removed by high-pass filtering built in the LIS2DH12.

Buffering Data

Another use case for interrupts comes on applications where we want to sample accelerometer at high intervals — vibration sensing for example. Waking the CPU up for every datapoint at high frequency would consume a lot of power, but we can use the built-in First-in-First-out (FIFO) buffer in LIS2DH12 to store up to 32 samples and read them all in one go. We can use one interrupt to trigger a FIFO read when the FIFO is full.

Interface

Our sensor interface does not support configuring generic interrupts, and we wish to keep the interface simple. Therefore we won’t be extending the sensor interface itself, but we rather extend our LIS2DH12 interface. This of course has a downside of tying any application to LIS2DH12 rather than using a generic accelerometer driver, but on the other hand we can always extend the accelerometer interface if the need arises.

We’ll add 4 functions to the LIS2DH12 interface:

/**
 * Enable 32-level FIFO in LIS2DH12
 * If FIFO is enabled, values are stored on LIS2DH12 FIFO and oldest element is returned on data read.
 *
 * parameter enable: true to enable FIFO, false to disable or reset FIFO.
 * return: RUUVI_DRIVER_SUCCESS on success, error code from stack on error.
 */
ruuvi_driver_status_t ruuvi_interface_lis2dh12_fifo_use(const bool enable);

/**
 * Read FIFO
 * Reads up to num_elements data points from FIFO and populates pointer data with them
 *
 * parameter num_elements: Input: number of elements in data. Output: Number of elements placed in data
 * parameter data: array of ruuvi_interface_acceleration_data_t with num_elements slots.
 * return: RUUVI_DRIVER_SUCCESS on success
 * return: RUUVI_DRIVER_ERROR_NULL if either parameter is NULL
 * return: RUUVI_DRIVER_ERROR_INVALID_STATE if FIFO is not in use
 * return: error code from stack on error.
 */
ruuvi_driver_status_t ruuvi_interface_lis2dh12_fifo_read(size_t* num_elements, ruuvi_interface_acceleration_data_t* data);

/**
 * Enable FIFO full interrupt on LIS2DH12.
 * Triggers as ACTIVE HIGH interrupt once FIFO has 32 elements.
 *
 * parameter enable: True to enable interrupt, false to disable interrupt
 * return: RUUVI_DRIVER_SUCCESS on success, error code from stack otherwise.
 **/
ruuvi_driver_status_t ruuvi_interface_lis2dh12_fifo_interrupt_use(const bool enable);

/**
 * Enable activity interrupt on LIS2DH12
 * Triggers as ACTIVE HIGH interrupt while detected movement is above threshold limit_g
 * Axes are high-passed for this interrupt, i.e. gravity won't trigger the interrupt
 * Axes are examined individually, compound acceleration won't trigger the interrupt.
 *
 * parameter enable:  True to enable interrupt, false to disable interrupt
 * parameter limit_g: Desired acceleration to trigger the interrupt.
 *                    Is considered as "at least", the acceleration is rounded up to next value.
 *                    Is written with value that was set to interrupt
 * returns: RUUVI_DRIVER_SUCCESS on success
 * returns: RUUVI_DRIVER_INVALID_STATE if acceleration limit is higher than maximum scale
 * returns: error code from stack on error.
 *
 */
ruuvi_driver_status_t ruuvi_interface_lis2dh12_activity_interrupt_use(const bool enable, float* limit_g);

View raw

First function enables the FIFO buffer, second function is for reading the data from the FIFO, third is for enabling the interrupt on FIFO full and fourth is for enabling activity interrupt. These functions are really stripped down versions of the features that LIS2DH12 would allow, but simple is good.

Driver

Our driver is essentially hard-coded choices: We select LIS2DH12 interrupt 1 as our FIFO interrupt and interrupt 2 as activity interrupt. While LIS2DH12 would support configurable interrupt level on FIFO, we leave the interrupt at a fixed level of 32 samples.

Same goes for activity interrupt, we fix the interrupt on “When high-passed movement on any axis exceeds threshold”. The threshold is configurable, and will be automatically converted from mg to counts by the driver.

Testing

While we’re working with the sensors, let’s add a couple more tests to sensor drivers. This time we add tests to verify that sensor modes works as we expect:

  • Sensor must be in SLEEP mode after init
  • Sensor must return all values as INVALID if sensor is read before first sample
  • Sensor must return to SLEEP mode after mode has been set to SINGLE
  • Sensor must have new data after setting mode to SINGLE returns
  • Sensor must same values, including timestamp, on successive calls to DATA_GET after SINGLE sample
  • Sensor must stay in CONTINUOUS mode after being set to continuous
  • Sensor must return RUUVI_DRIVER_ERROR_INVALID_STATE if set to SINGLE while in continuous mode and remain in continuous mode
  • Sensor must return RUUVI_DRIVER_ERROR_NULL if null mode is passed as a parameter
  • Sensor must return updated data in CONTINUOUS mode
A debug terminal test coverage
Our test coverage is growing, time to squash some bugs

Once again we find out that there’s numerous corner cases our drivers don’t yet handle — for example our drivers don’t return explicitly invalid values after init before sampling has started. Fixing these issues is easy, we track down individual errors as we did in part 8.

Tasks

This time we need to modify the task_acceleration and task_advertisement. Namely we add interrupt functions to our task_accelerometer and enable the LIS2DH12 interrupts.

Our FIFO task prints out the buffer and our movement task increments the movement counter. We also add a function to get the movement counter. The snipped below has the additions.

static void task_acceleration_fifo_full_task(void *p_event_data, uint16_t event_size)
{
  ruuvi_driver_status_t err_code = RUUVI_DRIVER_SUCCESS;
  ruuvi_interface_acceleration_data_t data[32];
  size_t data_len = sizeof(data);
  err_code |= ruuvi_interface_lis2dh12_fifo_read(&data_len, data);
  char msg[APPLICATION_LOG_BUFFER_SIZE] = { 0 };
  snprintf(msg, sizeof(msg), "%u: Read %u data points\r\n", (uint32_t)ruuvi_platform_rtc_millis(), data_len);
  ruuvi_platform_log(RUUVI_INTERFACE_LOG_INFO, msg);
  /*for(int ii = 0; ii < data_len; ii++)
  {
    memset(msg, 0, sizeof(msg));
    snprintf(msg, sizeof(msg), "T: %lu; X: %.3f; Y: %.3f; Z: %.3f;\r\n", (uint32_t)(data[ii].timestamp_ms&0xFFFFFFFF), data[ii].x_g, data[ii].y_g, data[ii].z_g);
    ruuvi_platform_log(RUUVI_INTERFACE_LOG_DEBUG, msg);
    ruuvi_platform_delay_ms(1);
  }
  */
  RUUVI_DRIVER_ERROR_CHECK(err_code, RUUVI_DRIVER_SUCCESS);
}

static void on_fifo (ruuvi_interface_gpio_evt_t event)
{
  ruuvi_platform_scheduler_event_put(NULL, 0, task_acceleration_fifo_full_task);
}

static void on_movement (ruuvi_interface_gpio_evt_t event)
{
  ruuvi_platform_log(RUUVI_INTERFACE_LOG_DEBUG, "Activity\r\n");
  m_nbr_movements++;
}

#if RUUVI_BOARD_ACCELEROMETER_LIS2DH12_PRESENT
    err_code = RUUVI_DRIVER_SUCCESS;
    // Only SPI supported for now
    bus = RUUVI_DRIVER_BUS_SPI;
    handle = RUUVI_BOARD_SPI_SS_ACCELEROMETER_PIN;
    err_code |= ruuvi_interface_lis2dh12_init(&acceleration_sensor, bus, handle);
    RUUVI_DRIVER_ERROR_CHECK(err_code, RUUVI_DRIVER_ERROR_NOT_FOUND);

    if(RUUVI_DRIVER_SUCCESS == err_code)
    {
      err_code |= task_acceleration_configure();

      err_code |= ruuvi_interface_lis2dh12_fifo_use(true);
      err_code |= ruuvi_interface_lis2dh12_fifo_interrupt_use(true);
      float ths = APPLICATION_ACCELEROMETER_ACTIVITY_THRESHOLD;
      err_code |= ruuvi_interface_lis2dh12_activity_interrupt_use(true, &ths);

      // Let pins settle
      ruuvi_platform_delay_ms(10);
      // Setup FIFO and activity interrupts
      err_code |= ruuvi_platform_gpio_interrupt_enable(RUUVI_BOARD_INT_ACC1_PIN, RUUVI_INTERFACE_GPIO_SLOPE_LOTOHI, RUUVI_INTERFACE_GPIO_MODE_INPUT_NOPULL, on_fifo);
      err_code |= ruuvi_platform_gpio_interrupt_enable(RUUVI_BOARD_INT_ACC2_PIN, RUUVI_INTERFACE_GPIO_SLOPE_LOTOHI, RUUVI_INTERFACE_GPIO_MODE_INPUT_NOPULL, on_movement);
      char msg[APPLICATION_LOG_BUFFER_SIZE] = { 0 };
      snprintf(msg, sizeof(msg), "Configured interrupt threshold at %.3f mg\r\n", ths);
      ruuvi_platform_log(RUUVI_INTERFACE_LOG_INFO, msg);

      return err_code;
    }


ruuvi_driver_status_t task_acceleration_movement_count_get(uint8_t * const count)
{
  *count = m_nbr_movements;
  return RUUVI_DRIVER_SUCCESS;
}

  #endif

View raw

Note the diffrent handling of the events: Incrementing a counter in activity interrupt is a quick event and we can run it in interrupt context. However, reading and printing out the FIFO takes time and we’ll schedule the action to a later time.

Now our tests are passing and FIFO is printed out to the terminal. If you run the final code, please note that the FIFO print is at DEBUG level and adjust your log level accordingly.

The FIFO print is at DEBUG level

Data Format 5

Missing part for the data format 5 has been the motion detection. We’ll add ruuvi_endpoint_5 which takes required data as input struct and encodes the data into given buffer.

Graphs with added voltage readout
Getting there — time to debug the voltage readout

The problem with the voltage readout turned out to be a simple omission in encoding. Another thing for the tests: Check that the encoding functions produce expected results.

Power Profile

We can expect quite a bit increased power consumption because we’re running the accelerometer at 10 Hz instead of previous 1 Hz as well as because of the FIFO reads. Let’s check it out.

Power profile that hasn't changed much
Our power profile hasn’t changed that much — but the FIFO read is clearly visible

Our last result was a bit under 23 μA, so now we have added approximately 7.5 μA to our consumption figure. Adding 33 % power consumption for FIFO read which is not used for anything is not really reasonable, so we’ll add a compiler flag to make FIFO optional in the future.

Conclusion

We now have the groundwork for making applications with accelerometer. However, we have only scratched the surface of the applications made possible with acceleration data. One of the key missing pieces is fast and reliable readout of data. This will be made soon possible with the GATT profile.

Stay tuned and follow @ojousima and @ruuvicom on Twitter for #FirmwareFriday posts!