F3

Dependencies:   mbed LSM6DS33_GR1

Fusion/FusionAhrs.c

Committer:
gr66
Date:
2021-09-30
Revision:
7:10653d0475f5

File content as of revision 7:10653d0475f5:

/**
 * @file FusionAhrs.c
 * @author Seb Madgwick
 * @brief The AHRS sensor fusion algorithm to combines gyroscope, accelerometer,
 * and magnetometer measurements into a single measurement of orientation
 * relative to the Earth (NWU convention).
 *
 * The algorithm behaviour is governed by a gain.  A low gain will decrease the
 * influence of the accelerometer and magnetometer so that the algorithm will
 * better reject disturbances causes by translational motion and temporary
 * magnetic distortions.  However, a low gain will also increase the risk of
 * drift due to gyroscope calibration errors.  A typical gain value suitable for
 * most applications is 0.5.
 *
 * The algorithm allows the application to define a minimum and maximum valid
 * magnetic field magnitude.  The algorithm will ignore magnetic measurements
 * that fall outside of this range.  This allows the algorithm to reject
 * magnetic measurements that do not represent the direction of magnetic North.
 * The typical magnitude of the Earth's magnetic field is between 20 uT and
 * 70 uT.
 *
 * The algorithm can be used without a magnetometer.  Measurements of
 * orientation obtained using only gyroscope and accelerometer measurements
 * can be expected to drift in the yaw component of orientation only.  The
 * application can reset the drift in yaw by setting the yaw to a specified
 * angle at any time.
 *
 * The algorithm provides the measurement of orientation as a quaternion.  The
 * library includes functions for converting this quaternion to a rotation
 * matrix and Euler angles.
 *
 * The algorithm also provides a measurement of linear acceleration and Earth
 * acceleration.  Linear acceleration is equal to the accelerometer  measurement
 * with the 1 g of gravity removed.  Earth acceleration is a measurement of
 * linear acceleration in the Earth coordinate frame.
 */

//------------------------------------------------------------------------------
// Includes

#include "FusionAhrs.h"
#include <float.h> // FLT_MAX
#include <math.h> // atan2f, cosf, sinf

//------------------------------------------------------------------------------
// Definitions

/**
 * @brief Initial gain used during the initialisation period.  The gain used by
 * each algorithm iteration will ramp down from this initial gain to the
 * specified algorithm gain over the initialisation period.
 */
#define INITIAL_GAIN (10.0f)

/**
 * @brief Initialisation period (in seconds).
 */
#define INITIALISATION_PERIOD (3.0f)

//------------------------------------------------------------------------------
// Functions

/**
 * @brief Initialises the AHRS algorithm structure.
 * @param fusionAhrs AHRS algorithm structure.
 * @param gain AHRS algorithm gain.
 */
void FusionAhrsInitialise(FusionAhrs * const fusionAhrs, const float gain) {
    fusionAhrs->gain = gain;
    fusionAhrs->minimumMagneticFieldSquared = 0.0f;
    fusionAhrs->maximumMagneticFieldSquared = FLT_MAX;
    fusionAhrs->quaternion = FUSION_QUATERNION_IDENTITY;
    fusionAhrs->linearAcceleration = FUSION_VECTOR3_ZERO;
    fusionAhrs->rampedGain = INITIAL_GAIN;
    fusionAhrs->zeroYawPending = false;
}

/**
 * @brief Sets the AHRS algorithm gain.  The gain must be equal or greater than
 * zero.
 * @param gain AHRS algorithm gain.
 */
void FusionAhrsSetGain(FusionAhrs * const fusionAhrs, const float gain) {
    fusionAhrs->gain = gain;
}

/**
 * @brief Sets the minimum and maximum valid magnetic field magnitudes in uT.
 * @param fusionAhrs AHRS algorithm structure.
 * @param minimumMagneticField Minimum valid magnetic field magnitude.
 * @param maximumMagneticField Maximum valid magnetic field magnitude.
 */
void FusionAhrsSetMagneticField(FusionAhrs * const fusionAhrs, const float minimumMagneticField, const float maximumMagneticField) {
    fusionAhrs->minimumMagneticFieldSquared = minimumMagneticField * minimumMagneticField;
    fusionAhrs->maximumMagneticFieldSquared = maximumMagneticField * maximumMagneticField;
}

/**
 * @brief Updates the AHRS algorithm.  This function should be called for each
 * new gyroscope measurement.
 * @param fusionAhrs AHRS algorithm structure.
 * @param gyroscope Gyroscope measurement in degrees per second.
 * @param accelerometer Accelerometer measurement in g.
 * @param magnetometer Magnetometer measurement in uT.
 * @param samplePeriod Sample period in seconds.  This is the difference in time
 * between the current and previous gyroscope measurements.
 */
void FusionAhrsUpdate(FusionAhrs * const fusionAhrs, const FusionVector3 gyroscope, const FusionVector3 accelerometer, const FusionVector3 magnetometer, const float samplePeriod) {
#define Q fusionAhrs->quaternion.element // define shorthand label for more readable code

    // Calculate feedback error
    FusionVector3 halfFeedbackError = FUSION_VECTOR3_ZERO; // scaled by 0.5 to avoid repeated multiplications by 2
    do {
        // Abandon feedback calculation if accelerometer measurement invalid
        if ((accelerometer.axis.x == 0.0f) && (accelerometer.axis.y == 0.0f) && (accelerometer.axis.z == 0.0f)) {
            break;
        }

        // Calculate direction of gravity assumed by quaternion
        const FusionVector3 halfGravity = {
            .axis.x = Q.x * Q.z - Q.w * Q.y,
            .axis.y = Q.w * Q.x + Q.y * Q.z,
            .axis.z = Q.w * Q.w - 0.5f + Q.z * Q.z,
        }; // equal to 3rd column of rotation matrix representation scaled by 0.5

        // Calculate accelerometer feedback error
        halfFeedbackError = FusionVectorCrossProduct(FusionVectorFastNormalise(accelerometer), halfGravity);

        // Abandon magnetometer feedback calculation if magnetometer measurement invalid
        const float magnetometerMagnitudeSquared = FusionVectorMagnitudeSquared(magnetometer);
        if ((magnetometerMagnitudeSquared < fusionAhrs->minimumMagneticFieldSquared) || (magnetometerMagnitudeSquared > fusionAhrs->maximumMagneticFieldSquared)) {
            break;
        }

        // Compute direction of 'magnetic west' assumed by quaternion
        const FusionVector3 halfWest = {
            .axis.x = Q.x * Q.y + Q.w * Q.z,
            .axis.y = Q.w * Q.w - 0.5f + Q.y * Q.y,
            .axis.z = Q.y * Q.z - Q.w * Q.x
        }; // equal to 2nd column of rotation matrix representation scaled by 0.5

        // Calculate magnetometer feedback error
        halfFeedbackError = FusionVectorAdd(halfFeedbackError, FusionVectorCrossProduct(FusionVectorFastNormalise(FusionVectorCrossProduct(accelerometer, magnetometer)), halfWest));

    } while (false);

    // Ramp down gain until initialisation complete
    if (fusionAhrs->gain == 0) {
        fusionAhrs->rampedGain = 0; // skip initialisation if gain is zero
    }
    float feedbackGain = fusionAhrs->gain;
    if (fusionAhrs->rampedGain > fusionAhrs->gain) {
        fusionAhrs->rampedGain -= (INITIAL_GAIN - fusionAhrs->gain) * samplePeriod / INITIALISATION_PERIOD;
        feedbackGain = fusionAhrs->rampedGain;
    }

    // Convert gyroscope to radians per second scaled by 0.5
    FusionVector3 halfGyroscope = FusionVectorMultiplyScalar(gyroscope, 0.5f * FusionDegreesToRadians(1.0f));

    // Apply feedback to gyroscope
    halfGyroscope = FusionVectorAdd(halfGyroscope, FusionVectorMultiplyScalar(halfFeedbackError, feedbackGain));

    // Integrate rate of change of quaternion
    fusionAhrs->quaternion = FusionQuaternionAdd(fusionAhrs->quaternion, FusionQuaternionMultiplyVector(fusionAhrs->quaternion, FusionVectorMultiplyScalar(halfGyroscope, samplePeriod)));

    // Normalise quaternion
    fusionAhrs->quaternion = FusionQuaternionFastNormalise(fusionAhrs->quaternion);

    // Calculate linear acceleration
    const FusionVector3 gravity = {
        .axis.x = 2.0f * (Q.x * Q.z - Q.w * Q.y),
        .axis.y = 2.0f * (Q.w * Q.x + Q.y * Q.z),
        .axis.z = 2.0f * (Q.w * Q.w - 0.5f + Q.z * Q.z),
    }; // equal to 3rd column of rotation matrix representation
    fusionAhrs->linearAcceleration = FusionVectorSubtract(accelerometer, gravity);

#undef Q // undefine shorthand label
}

/**
 * @brief Updates the AHRS algorithm.  This function should be called for each
 * new gyroscope measurement.
 * @param fusionAhrs AHRS algorithm structure.
 * @param gyroscope Gyroscope measurement in degrees per second.
 * @param accelerometer Accelerometer measurement in g.
 * @param samplePeriod Sample period in seconds.  This is the difference in time
 * between the current and previous gyroscope measurements.
 */
void FusionAhrsUpdateWithoutMagnetometer(FusionAhrs * const fusionAhrs, const FusionVector3 gyroscope, const FusionVector3 accelerometer, const float samplePeriod) {

    // Update AHRS algorithm
    FusionAhrsUpdate(fusionAhrs, gyroscope, accelerometer, FUSION_VECTOR3_ZERO, samplePeriod);

    // Zero yaw once initialisation complete
    if (FusionAhrsIsInitialising(fusionAhrs) == true) {
        fusionAhrs->zeroYawPending = true;
    } else {
        if (fusionAhrs->zeroYawPending == true) {
            FusionAhrsSetYaw(fusionAhrs, 0.0f);
            fusionAhrs->zeroYawPending = false;
        }
    }
}

/**
 * @brief Gets the quaternion describing the sensor relative to the Earth.
 * @param fusionAhrs AHRS algorithm structure.
 * @return Quaternion describing the sensor relative to the Earth.
 */
FusionQuaternion FusionAhrsGetQuaternion(const FusionAhrs * const fusionAhrs) {
    return FusionQuaternionConjugate(fusionAhrs->quaternion);
}

/**
 * @brief Gets the linear acceleration measurement equal to the accelerometer
 * measurement with the 1 g of gravity removed.
 * @param fusionAhrs AHRS algorithm structure.
 * @return Linear acceleration measurement.
 */
FusionVector3 FusionAhrsGetLinearAcceleration(const FusionAhrs * const fusionAhrs) {
    return fusionAhrs->linearAcceleration;
}

/**
 * @brief Gets the Earth acceleration measurement equal to linear acceleration
 * in the Earth coordinate frame.
 * @param fusionAhrs AHRS algorithm structure.
 * @return Earth acceleration measurement.
 */
FusionVector3 FusionAhrsGetEarthAcceleration(const FusionAhrs * const fusionAhrs) {
#define Q fusionAhrs->quaternion.element // define shorthand labels for more readable code
#define A fusionAhrs->linearAcceleration.axis
    const float qwqw = Q.w * Q.w; // calculate common terms to avoid repeated operations
    const float qwqx = Q.w * Q.x;
    const float qwqy = Q.w * Q.y;
    const float qwqz = Q.w * Q.z;
    const float qxqy = Q.x * Q.y;
    const float qxqz = Q.x * Q.z;
    const float qyqz = Q.y * Q.z;
    const FusionVector3 earthAcceleration = {
        .axis.x = 2.0f * ((qwqw - 0.5f + Q.x * Q.x) * A.x + (qxqy - qwqz) * A.y + (qxqz + qwqy) * A.z),
        .axis.y = 2.0f * ((qxqy + qwqz) * A.x + (qwqw - 0.5f + Q.y * Q.y) * A.y + (qyqz - qwqx) * A.z),
        .axis.z = 2.0f * ((qxqz - qwqy) * A.x + (qyqz + qwqx) * A.y + (qwqw - 0.5f + Q.z * Q.z) * A.z),
    }; // transpose of a rotation matrix representation of the quaternion multiplied with the linear acceleration
    return earthAcceleration;
#undef Q // undefine shorthand label
#undef A
}

/**
 * @brief Reinitialise the AHRS algorithm.
 * @param fusionAhrs AHRS algorithm structure.
 */
void FusionAhrsReinitialise(FusionAhrs * const fusionAhrs) {
    fusionAhrs->quaternion = FUSION_QUATERNION_IDENTITY;
    fusionAhrs->linearAcceleration = FUSION_VECTOR3_ZERO;
    fusionAhrs->rampedGain = INITIAL_GAIN;
}

/**
 * @brief Returns true while the AHRS algorithm is initialising.
 * @param fusionAhrs AHRS algorithm structure.
 * @return True while the AHRS algorithm is initialising.
 */
bool FusionAhrsIsInitialising(const FusionAhrs * const fusionAhrs) {
    return fusionAhrs->rampedGain > fusionAhrs->gain;
}

/**
 * @brief Sets the yaw component of the orientation measurement provided by the
 * AHRS algorithm.  This function can be used to reset drift in yaw when the
 * AHRS algorithm is being used without a magnetometer.
 * @param fusionAhrs AHRS algorithm structure.
 * @param yaw Yaw angle in degrees.
 */
void FusionAhrsSetYaw(FusionAhrs * const fusionAhrs, const float yaw) {
#define Q fusionAhrs->quaternion.element // define shorthand label for more readable code
    fusionAhrs->quaternion = FusionQuaternionNormalise(fusionAhrs->quaternion); // quaternion must be normalised accurately (approximation not sufficient)
    const float inverseYaw = atan2f(Q.x * Q.y + Q.w * Q.z, Q.w * Q.w - 0.5f + Q.x * Q.x); // Euler angle of conjugate
    const float halfInverseYawMinusOffset = 0.5f * (inverseYaw - FusionDegreesToRadians(yaw));
    const FusionQuaternion inverseYawQuaternion = {
        .element.w = cosf(halfInverseYawMinusOffset),
        .element.x = 0.0f,
        .element.y = 0.0f,
        .element.z = -1.0f * sinf(halfInverseYawMinusOffset),
    };
    fusionAhrs->quaternion = FusionQuaternionMultiply(inverseYawQuaternion, fusionAhrs->quaternion);
#undef Q // undefine shorthand label
}

//------------------------------------------------------------------------------
// End of file