My fork

Dependencies:   BLE_API nRF51822-bluetooth-mdw

Fork of microbit-dal by Lancaster University

Revision:
1:8aa5cdb4ab67
Child:
6:2e1c2e0d8c7a
diff -r fb15f7887843 -r 8aa5cdb4ab67 source/drivers/MicroBitAccelerometer.cpp
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/source/drivers/MicroBitAccelerometer.cpp	Thu Apr 07 01:33:22 2016 +0100
@@ -0,0 +1,710 @@
+/*
+The MIT License (MIT)
+
+Copyright (c) 2016 British Broadcasting Corporation.
+This software is provided by Lancaster University by arrangement with the BBC.
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the "Software"),
+to deal in the Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
+*/
+
+/**
+ * Class definition for MicroBit Accelerometer.
+ *
+ * Represents an implementation of the Freescale MMA8653 3 axis accelerometer
+ * Also includes basic data caching and on demand activation.
+ */
+#include "MicroBitConfig.h"
+#include "MicroBitAccelerometer.h"
+#include "ErrorNo.h"
+#include "MicroBitConfig.h"
+#include "MicroBitEvent.h"
+#include "MicroBitCompat.h"
+#include "MicroBitFiber.h"
+
+/**
+  * Configures the accelerometer for G range and sample rate defined
+  * in this object. The nearest values are chosen to those defined
+  * that are supported by the hardware. The instance variables are then
+  * updated to reflect reality.
+  *
+  * @return MICROBIT_OK on success, MICROBIT_I2C_ERROR if the accelerometer could not be configured.
+  */
+int MicroBitAccelerometer::configure()
+{
+    const MMA8653SampleRangeConfig  *actualSampleRange;
+    const MMA8653SampleRateConfig  *actualSampleRate;
+    int result;
+
+    // First find the nearest sample rate to that specified.
+    actualSampleRate = &MMA8653SampleRate[MMA8653_SAMPLE_RATES-1];
+    for (int i=MMA8653_SAMPLE_RATES-1; i>=0; i--)
+    {
+        if(MMA8653SampleRate[i].sample_period < this->samplePeriod * 1000)
+            break;
+
+        actualSampleRate = &MMA8653SampleRate[i];
+    }
+
+    // Now find the nearest sample range to that specified.
+    actualSampleRange = &MMA8653SampleRange[MMA8653_SAMPLE_RANGES-1];
+    for (int i=MMA8653_SAMPLE_RANGES-1; i>=0; i--)
+    {
+        if(MMA8653SampleRange[i].sample_range < this->sampleRange)
+            break;
+
+        actualSampleRange = &MMA8653SampleRange[i];
+    }
+
+    // OK, we have the correct data. Update our local state.
+    this->samplePeriod = actualSampleRate->sample_period / 1000;
+    this->sampleRange = actualSampleRange->sample_range;
+
+    // Now configure the accelerometer accordingly.
+    // First place the device into standby mode, so it can be configured.
+    result = writeCommand(MMA8653_CTRL_REG1, 0x00);
+    if (result != 0)
+        return MICROBIT_I2C_ERROR;
+
+    // Enable high precisiosn mode. This consumes a bit more power, but still only 184 uA!
+    result = writeCommand(MMA8653_CTRL_REG2, 0x10);
+    if (result != 0)
+        return MICROBIT_I2C_ERROR;
+
+    // Enable the INT1 interrupt pin.
+    result = writeCommand(MMA8653_CTRL_REG4, 0x01);
+    if (result != 0)
+        return MICROBIT_I2C_ERROR;
+
+    // Select the DATA_READY event source to be routed to INT1
+    result = writeCommand(MMA8653_CTRL_REG5, 0x01);
+    if (result != 0)
+        return MICROBIT_I2C_ERROR;
+
+    // Configure for the selected g range.
+    result = writeCommand(MMA8653_XYZ_DATA_CFG, actualSampleRange->xyz_data_cfg);
+    if (result != 0)
+        return MICROBIT_I2C_ERROR;
+
+    // Bring the device back online, with 10bit wide samples at the requested frequency.
+    result = writeCommand(MMA8653_CTRL_REG1, actualSampleRate->ctrl_reg1 | 0x01);
+    if (result != 0)
+        return MICROBIT_I2C_ERROR;
+
+    return MICROBIT_OK;
+}
+
+/**
+  * Issues a standard, 2 byte I2C command write to the accelerometer.
+  *
+  * Blocks the calling thread until complete.
+  *
+  * @param reg The address of the register to write to.
+  *
+  * @param value The value to write.
+  *
+  * @return MICROBIT_OK on success, MICROBIT_I2C_ERROR if the the write request failed.
+  */
+int MicroBitAccelerometer::writeCommand(uint8_t reg, uint8_t value)
+{
+    uint8_t command[2];
+    command[0] = reg;
+    command[1] = value;
+
+    return i2c.write(address, (const char *)command, 2);
+}
+
+/**
+  * Issues a read command, copying data into the specified buffer.
+  *
+  * Blocks the calling thread until complete.
+  *
+  * @param reg The address of the register to access.
+  *
+  * @param buffer Memory area to read the data into.
+  *
+  * @param length The number of bytes to read.
+  *
+  * @return MICROBIT_OK on success, MICROBIT_INVALID_PARAMETER or MICROBIT_I2C_ERROR if the the read request failed.
+  */
+int MicroBitAccelerometer::readCommand(uint8_t reg, uint8_t* buffer, int length)
+{
+    int result;
+
+    if (buffer == NULL || length <= 0 )
+        return MICROBIT_INVALID_PARAMETER;
+
+    result = i2c.write(address, (const char *)&reg, 1, true);
+    if (result !=0)
+        return MICROBIT_I2C_ERROR;
+
+    result = i2c.read(address, (char *)buffer, length);
+    if (result !=0)
+        return MICROBIT_I2C_ERROR;
+
+    return MICROBIT_OK;
+}
+
+/**
+  * Constructor.
+  * Create a software abstraction of an accelerometer.
+  *
+  * @param _i2c an instance of MicroBitI2C used to communicate with the onboard accelerometer.
+  *
+  * @param address the default I2C address of the accelerometer. Defaults to: MMA8653_DEFAULT_ADDR.
+  *
+  * @param id the unique EventModel id of this component. Defaults to: MICROBIT_ID_ACCELEROMETER
+  *
+  * @code
+  * MicroBitI2C i2c = MicroBitI2C(I2C_SDA0, I2C_SCL0);
+  *
+  * MicroBitAccelerometer accelerometer = MicroBitAccelerometer(i2c);
+  * @endcode
+ */
+MicroBitAccelerometer::MicroBitAccelerometer(MicroBitI2C& _i2c, uint16_t address, uint16_t id) : sample(), int1(MICROBIT_PIN_ACCEL_DATA_READY), i2c(_i2c)
+{
+    // Store our identifiers.
+    this->id = id;
+    this->status = 0;
+    this->address = address;
+
+    // Update our internal state for 50Hz at +/- 2g (50Hz has a period af 20ms).
+    this->samplePeriod = 20;
+    this->sampleRange = 2;
+
+    // Initialise gesture history
+    this->sigma = 0;
+    this->lastGesture = GESTURE_NONE;
+    this->currentGesture = GESTURE_NONE;
+    this->shake.x = 0;
+    this->shake.y = 0;
+    this->shake.z = 0;
+    this->shake.count = 0;
+    this->shake.timer = 0;
+
+    // Configure and enable the accelerometer.
+    if (this->configure() == MICROBIT_OK)
+        status |= MICROBIT_COMPONENT_RUNNING;
+}
+
+/**
+  * Attempts to read the 8 bit ID from the accelerometer, this can be used for
+  * validation purposes.
+  *
+  * @return the 8 bit ID returned by the accelerometer, or MICROBIT_I2C_ERROR if the request fails.
+  *
+  * @code
+  * accelerometer.whoAmI();
+  * @endcode
+  */
+int MicroBitAccelerometer::whoAmI()
+{
+    uint8_t data;
+    int result;
+
+    result = readCommand(MMA8653_WHOAMI, &data, 1);
+    if (result !=0)
+        return MICROBIT_I2C_ERROR;
+
+    return (int)data;
+}
+
+/**
+  * Reads the acceleration data from the accelerometer, and stores it in our buffer.
+  * This only happens if the accelerometer indicates that it has new data via int1.
+  *
+  * On first use, this member function will attempt to add this component to the
+  * list of fiber components in order to constantly update the values stored
+  * by this object.
+  *
+  * This technique is called lazy instantiation, and it means that we do not
+  * obtain the overhead from non-chalantly adding this component to fiber components.
+  *
+  * @return MICROBIT_OK on success, MICROBIT_I2C_ERROR if the read request fails.
+  */
+int MicroBitAccelerometer::updateSample()
+{
+    if(!(status & MICROBIT_ACCEL_ADDED_TO_IDLE))
+    {
+        fiber_add_idle_component(this);
+        status |= MICROBIT_ACCEL_ADDED_TO_IDLE;
+    }
+
+    // Poll interrupt line from accelerometer.
+    // n.b. Default is Active LO. Interrupt is cleared in data read.
+    if(!int1)
+    {
+        int8_t data[6];
+        int result;
+
+        result = readCommand(MMA8653_OUT_X_MSB, (uint8_t *)data, 6);
+        if (result !=0)
+            return MICROBIT_I2C_ERROR;
+
+        // read MSB values...
+        sample.x = data[0];
+        sample.y = data[2];
+        sample.z = data[4];
+
+        // Normalize the data in the 0..1024 range.
+        sample.x *= 8;
+        sample.y *= 8;
+        sample.z *= 8;
+
+#if CONFIG_ENABLED(USE_ACCEL_LSB)
+        // Add in LSB values.
+        sample.x += (data[1] / 64);
+        sample.y += (data[3] / 64);
+        sample.z += (data[5] / 64);
+#endif
+
+        // Scale into millig (approx!)
+        sample.x *= this->sampleRange;
+        sample.y *= this->sampleRange;
+        sample.z *= this->sampleRange;
+
+        // Indicate that pitch and roll data is now stale, and needs to be recalculated if needed.
+        status &= ~MICROBIT_ACCEL_PITCH_ROLL_VALID;
+
+        // Update gesture tracking
+        updateGesture();
+
+        // Indicate that a new sample is available
+        MicroBitEvent e(id, MICROBIT_ACCELEROMETER_EVT_DATA_UPDATE);
+    }
+
+    return MICROBIT_OK;
+};
+
+/**
+  * A service function.
+  * It calculates the current scalar acceleration of the device (x^2 + y^2 + z^2).
+  * It does not, however, square root the result, as this is a relatively high cost operation.
+  *
+  * This is left to application code should it be needed.
+  *
+  * @return the sum of the square of the acceleration of the device across all axes.
+  */
+int MicroBitAccelerometer::instantaneousAccelerationSquared()
+{
+    updateSample();
+
+    // Use pythagoras theorem to determine the combined force acting on the device.
+    return (int)sample.x*(int)sample.x + (int)sample.y*(int)sample.y + (int)sample.z*(int)sample.z;
+}
+
+/**
+ * Service function.
+ * Determines a 'best guess' posture of the device based on instantaneous data.
+ *
+ * This makes no use of historic data, and forms this input to the filter implemented in updateGesture().
+ *
+ * @return A 'best guess' of the current posture of the device, based on instanataneous data.
+ */
+BasicGesture MicroBitAccelerometer::instantaneousPosture()
+{
+    int force = instantaneousAccelerationSquared();
+    bool shakeDetected = false;
+
+    // Test for shake events.
+    // We detect a shake by measuring zero crossings in each axis. In other words, if we see a strong acceleration to the left followed by
+    // a string acceleration to the right, then we can infer a shake. Similarly, we can do this for each acxis (left/right, up/down, in/out).
+    //
+    // If we see enough zero crossings in succession (MICROBIT_ACCELEROMETER_SHAKE_COUNT_THRESHOLD), then we decide that the device
+    // has been shaken.
+    if ((getX() < -MICROBIT_ACCELEROMETER_SHAKE_TOLERANCE && shake.x) || (getX() > MICROBIT_ACCELEROMETER_SHAKE_TOLERANCE && !shake.x))
+    {
+        shakeDetected = true;
+        shake.x = !shake.x;
+    }
+
+    if ((getY() < -MICROBIT_ACCELEROMETER_SHAKE_TOLERANCE && shake.y) || (getY() > MICROBIT_ACCELEROMETER_SHAKE_TOLERANCE && !shake.y))
+    {
+        shakeDetected = true;
+        shake.y = !shake.y;
+    }
+
+    if ((getZ() < -MICROBIT_ACCELEROMETER_SHAKE_TOLERANCE && shake.z) || (getZ() > MICROBIT_ACCELEROMETER_SHAKE_TOLERANCE && !shake.z))
+    {
+        shakeDetected = true;
+        shake.z = !shake.z;
+    }
+
+    if (shakeDetected && shake.count < MICROBIT_ACCELEROMETER_SHAKE_COUNT_THRESHOLD && ++shake.count == MICROBIT_ACCELEROMETER_SHAKE_COUNT_THRESHOLD)
+        shake.shaken = 1;
+
+    if (++shake.timer >= MICROBIT_ACCELEROMETER_SHAKE_DAMPING)
+    {
+        shake.timer = 0;
+        if (shake.count > 0)
+        {
+            if(--shake.count == 0)
+                shake.shaken = 0;
+        }
+    }
+
+    if (shake.shaken)
+        return GESTURE_SHAKE;
+
+    if (force < MICROBIT_ACCELEROMETER_FREEFALL_THRESHOLD)
+        return GESTURE_FREEFALL;
+
+    if (force > MICROBIT_ACCELEROMETER_3G_THRESHOLD)
+        return GESTURE_3G;
+
+    if (force > MICROBIT_ACCELEROMETER_6G_THRESHOLD)
+        return GESTURE_6G;
+
+    if (force > MICROBIT_ACCELEROMETER_8G_THRESHOLD)
+        return GESTURE_8G;
+
+    // Determine our posture.
+    if (getX() < (-1000 + MICROBIT_ACCELEROMETER_TILT_TOLERANCE))
+        return GESTURE_LEFT;
+
+    if (getX() > (1000 - MICROBIT_ACCELEROMETER_TILT_TOLERANCE))
+        return GESTURE_RIGHT;
+
+    if (getY() < (-1000 + MICROBIT_ACCELEROMETER_TILT_TOLERANCE))
+        return GESTURE_DOWN;
+
+    if (getY() > (1000 - MICROBIT_ACCELEROMETER_TILT_TOLERANCE))
+        return GESTURE_UP;
+
+    if (getZ() < (-1000 + MICROBIT_ACCELEROMETER_TILT_TOLERANCE))
+        return GESTURE_FACE_UP;
+
+    if (getZ() > (1000 - MICROBIT_ACCELEROMETER_TILT_TOLERANCE))
+        return GESTURE_FACE_DOWN;
+
+    return GESTURE_NONE;
+}
+
+/**
+  * Updates the basic gesture recognizer. This performs instantaneous pose recognition, and also some low pass filtering to promote
+  * stability.
+  */
+void MicroBitAccelerometer::updateGesture()
+{
+    // Determine what it looks like we're doing based on the latest sample...
+    BasicGesture g = instantaneousPosture();
+
+    // Perform some low pass filtering to reduce jitter from any detected effects
+    if (g == currentGesture)
+    {
+        if (sigma < MICROBIT_ACCELEROMETER_GESTURE_DAMPING)
+            sigma++;
+    }
+    else
+    {
+        currentGesture = g;
+        sigma = 0;
+    }
+
+    // If we've reached threshold, update our record and raise the relevant event...
+    if (currentGesture != lastGesture && sigma >= MICROBIT_ACCELEROMETER_GESTURE_DAMPING)
+    {
+        lastGesture = currentGesture;
+        MicroBitEvent e(MICROBIT_ID_GESTURE, lastGesture);
+    }
+}
+
+/**
+  * Attempts to set the sample rate of the accelerometer to the specified value (in ms).
+  *
+  * @param period the requested time between samples, in milliseconds.
+  *
+  * @return MICROBIT_OK on success, MICROBIT_I2C_ERROR is the request fails.
+  *
+  * @code
+  * // sample rate is now 20 ms.
+  * accelerometer.setPeriod(20);
+  * @endcode
+  *
+  * @note The requested rate may not be possible on the hardware. In this case, the
+  * nearest lower rate is chosen.
+  */
+int MicroBitAccelerometer::setPeriod(int period)
+{
+    this->samplePeriod = period;
+    return this->configure();
+}
+
+/**
+  * Reads the currently configured sample rate of the accelerometer.
+  *
+  * @return The time between samples, in milliseconds.
+  */
+int MicroBitAccelerometer::getPeriod()
+{
+    return (int)samplePeriod;
+}
+
+/**
+  * Attempts to set the sample range of the accelerometer to the specified value (in g).
+  *
+  * @param range The requested sample range of samples, in g.
+  *
+  * @return MICROBIT_OK on success, MICROBIT_I2C_ERROR is the request fails.
+  *
+  * @code
+  * // the sample range of the accelerometer is now 8G.
+  * accelerometer.setRange(8);
+  * @endcode
+  *
+  * @note The requested range may not be possible on the hardware. In this case, the
+  * nearest lower range is chosen.
+  */
+int MicroBitAccelerometer::setRange(int range)
+{
+    this->sampleRange = range;
+    return this->configure();
+}
+
+/**
+  * Reads the currently configured sample range of the accelerometer.
+  *
+  * @return The sample range, in g.
+  */
+int MicroBitAccelerometer::getRange()
+{
+    return (int)sampleRange;
+}
+
+/**
+  * Reads the value of the X axis from the latest update retrieved from the accelerometer.
+  *
+  * @param system The coordinate system to use. By default, a simple cartesian system is provided.
+  *
+  * @return The force measured in the X axis, in milli-g.
+  *
+  * @code
+  * accelerometer.getX();
+  * @endcode
+  */
+int MicroBitAccelerometer::getX(MicroBitCoordinateSystem system)
+{
+    updateSample();
+
+    switch (system)
+    {
+        case SIMPLE_CARTESIAN:
+            return -sample.x;
+
+        case NORTH_EAST_DOWN:
+            return sample.y;
+
+        case RAW:
+        default:
+            return sample.x;
+    }
+}
+
+/**
+  * Reads the value of the Y axis from the latest update retrieved from the accelerometer.
+  *
+  * @return The force measured in the Y axis, in milli-g.
+  *
+  * @code
+  * accelerometer.getY();
+  * @endcode
+  */
+int MicroBitAccelerometer::getY(MicroBitCoordinateSystem system)
+{
+    updateSample();
+
+    switch (system)
+    {
+        case SIMPLE_CARTESIAN:
+            return -sample.y;
+
+        case NORTH_EAST_DOWN:
+            return -sample.x;
+
+        case RAW:
+        default:
+            return sample.y;
+    }
+}
+
+/**
+  * Reads the value of the Z axis from the latest update retrieved from the accelerometer.
+  *
+  * @return The force measured in the Z axis, in milli-g.
+  *
+  * @code
+  * accelerometer.getZ();
+  * @endcode
+  */
+int MicroBitAccelerometer::getZ(MicroBitCoordinateSystem system)
+{
+    updateSample();
+
+    switch (system)
+    {
+        case NORTH_EAST_DOWN:
+            return -sample.z;
+
+        case SIMPLE_CARTESIAN:
+        case RAW:
+        default:
+            return sample.z;
+    }
+}
+
+/**
+  * Provides a rotation compensated pitch of the device, based on the latest update retrieved from the accelerometer.
+  *
+  * @return The pitch of the device, in degrees.
+  *
+  * @code
+  * accelerometer.getPitch();
+  * @endcode
+  */
+int MicroBitAccelerometer::getPitch()
+{
+    return (int) ((360*getPitchRadians()) / (2*PI));
+}
+
+/**
+  * Provides a rotation compensated pitch of the device, based on the latest update retrieved from the accelerometer.
+  *
+  * @return The pitch of the device, in radians.
+  *
+  * @code
+  * accelerometer.getPitchRadians();
+  * @endcode
+  */
+float MicroBitAccelerometer::getPitchRadians()
+{
+    if (!(status & MICROBIT_ACCEL_PITCH_ROLL_VALID))
+        recalculatePitchRoll();
+
+    return pitch;
+}
+
+/**
+  * Provides a rotation compensated roll of the device, based on the latest update retrieved from the accelerometer.
+  *
+  * @return The roll of the device, in degrees.
+  *
+  * @code
+  * accelerometer.getRoll();
+  * @endcode
+  */
+int MicroBitAccelerometer::getRoll()
+{
+    return (int) ((360*getRollRadians()) / (2*PI));
+}
+
+/**
+  * Provides a rotation compensated roll of the device, based on the latest update retrieved from the accelerometer.
+  *
+  * @return The roll of the device, in radians.
+  *
+  * @code
+  * accelerometer.getRollRadians();
+  * @endcode
+  */
+float MicroBitAccelerometer::getRollRadians()
+{
+    if (!(status & MICROBIT_ACCEL_PITCH_ROLL_VALID))
+        recalculatePitchRoll();
+
+    return roll;
+}
+
+/**
+  * Recalculate roll and pitch values for the current sample.
+  *
+  * @note We only do this at most once per sample, as the necessary trigonemteric functions are rather
+  *       heavyweight for a CPU without a floating point unit.
+  */
+void MicroBitAccelerometer::recalculatePitchRoll()
+{
+    float x = (float) getX(NORTH_EAST_DOWN);
+    float y = (float) getY(NORTH_EAST_DOWN);
+    float z = (float) getZ(NORTH_EAST_DOWN);
+
+    roll = atan2(getY(NORTH_EAST_DOWN), getZ(NORTH_EAST_DOWN));
+    pitch = atan(-x / (y*sin(roll) + z*cos(roll)));
+    status |= MICROBIT_ACCEL_PITCH_ROLL_VALID;
+}
+
+/**
+  * Retrieves the last recorded gesture.
+  *
+  * @return The last gesture that was detected.
+  *
+  * Example:
+  * @code
+  * MicroBitDisplay display;
+  *
+  * if (accelerometer.getGesture() == SHAKE)
+  *     display.scroll("SHAKE!");
+  * @endcode
+  */
+BasicGesture MicroBitAccelerometer::getGesture()
+{
+    return lastGesture;
+}
+
+/**
+  * A periodic callback invoked by the fiber scheduler idle thread.
+  *
+  * Internally calls updateSample().
+  */
+void MicroBitAccelerometer::idleTick()
+{
+    updateSample();
+}
+
+/**
+  * Returns 0 or 1. 1 indicates data is waiting to be read, zero means data is not ready to be read.
+  *
+  * We check if any data is ready for reading by checking the interrupt flag on the accelerometer.
+  */
+int MicroBitAccelerometer::isIdleCallbackNeeded()
+{
+    return !int1;
+}
+
+/**
+  * Destructor for MicroBitAccelerometer, where we deregister from the array of fiber components.
+  */
+MicroBitAccelerometer::~MicroBitAccelerometer()
+{
+    fiber_remove_idle_component(this);
+}
+
+const MMA8653SampleRangeConfig MMA8653SampleRange[MMA8653_SAMPLE_RANGES] = {
+    {2, 0},
+    {4, 1},
+    {8, 2}
+};
+
+const MMA8653SampleRateConfig MMA8653SampleRate[MMA8653_SAMPLE_RATES] = {
+    {1250,      0x00},
+    {2500,      0x08},
+    {5000,      0x10},
+    {10000,     0x18},
+    {20000,     0x20},
+    {80000,     0x28},
+    {160000,    0x30},
+    {640000,    0x38}
+};