Copyright 2023 Travis J. West, Input Devices and Music Interaction Laboratory (IDMIL), Centre for Interdisciplinary Research in Music Media and Technology (CIRMMT), McGill University, Montréal, Canada, and Univ. Lille, Inria, CNRS, Centrale Lille, UMR 9189 CRIStAL, F-59000 Lille, France
SPDX-License-Identifier: MIT
This component performs runtime calibration and sensor fusion of magnetic and inertial measurements from a MIMU device such as the ICM20948.
#pragma once
#include "sygah-endpoints.hpp"
#include "sygah-mimu.hpp"
#include "sygsp-mimu_units.hpp"
namespace sygaldry { namespace sygsp {
struct ComplementaryMimuFusionInputs {
slider<"proportional feedback"
, "proportional feedback gain coefficient"
, float, 0.0f, 10.0f, 3.0f
, tag_session_data
> k_P;
slider<"integral feedback"
, "integral feedback gain coefficient"
, float, 0.0f, 10.0f, 0.0f
, tag_session_data
> k_I;
slider<"accelerometer influence"
, "accelerometer gain coefficient; reflects confidence in the accelerometer data"
, float, 0.0f, 1.0f, 0.25f
, tag_session_data
> k_a;
slider<"magnetometer influence"
, "magnetometer gain coefficient; reflects confidence in the magnetometer data"
, float, 0.0f, 1.0f, 1.0f
, tag_session_data
> k_m;
slider<"movement threshold"
, "threshold of jerk magnitude below which the device is considered as stationary"
, float, 0.0f, 32.0f, 0.0001f
, tag_session_data
> movement_threshold;
slider<"gyroscope bias smoothing"
, "IIR filter coefficient for gyro bias smoothing filter; higher value results in more smoothing/lower cutoff"
, float, 0.0f, 1.0f, 0.99f
, tag_session_data
> gyro_alpha;
bng<"initialize", "trigger sensor re-initialization"> initialize;
toggle<"calibrate magnetometer"
, "enable tracking magnetometer data to estimate and remove sensor bias; "
"the previous bias estimate is reset when compensation is enabled"
> calibrate_magnetometer;
};
struct ComplementaryMimuFusionOutputs {
array_message<"sensor to global quaternion", 4
, "quaternion representation of the orientation of the sensor in the global frame"
, float, -1.0f, 1.0f
> quaternion;
array_message<"global to sensor quaternion", 4
, "quaternion representation of the orientation of the global bases in the sensor frame"
, float, -1.0f, 1.0f
> conjugate;
array_message<"sensor to global matrix", 9
, "special orthogonal matrix of the sensor basis vectors expressed in the global frame"
, float, -1.0f, 1.0f
> matrix;
array_message<"global to sensor matrix", 9
, "special orthogonal matrix of the global basis vectors expressed in the sensor frame"
, float, -1.0f, 1.0f
> inverse;
slider<"norm of gravity", "estimated strength of gravity relative to one standard gravity (9.80665 m/s/s)"
, float, 0.995f, 1.002f
> norm_of_gravity;
slider<"gravity measurements", "count of gravity measurements"
, unsigned int, 0, 1<<16
> g_count;
vec3_message<"sensor acceleration"
, "m/s/s", "estimated acceleration due to motion expressed in the sensor frame of reference"
> sensor_accl;
vec3_message<"global acceleration"
, "m/s/s", "estimated acceleration due to motion expressed in the performance frame of reference"
> global_accl;
vec3_message<"previous accelerometer"
, float, -16.0f, 16.0f
, "g", "previous measurement of acceleration; used in the estimation of jerk"
> accl_prev;
vec3_message<"jerk"
, float, -32.0f, 32.0f
, "g/s", "approximate signal analogous to the derivative of acceleration in the sensor frame"
> jerk;
vec3_message<"gyroscope bias"
, "rad/s", "estimated gyroscope sensor bias"
> gyro_bias;
vec3_message<"magnetometer maximum"
, float, -4900.0f, 4900.0f
, "uT", "component-wise maximum of magnetometer values measured while calibrating bias compensation"
> magn_max;
vec3_message<"magnetometer minimum"
, float, -4900.0f, 4900.0f
, "uT", "component-wise minimum of magnetometer values measured while calibrating bias compensation"
> magn_min;
vec3_message<"magnetometer bias"
, float, -4900.0f, 4900.0f
, "uT", "estimated magnetometer sensor bias"
> magn_bias;
vec3_message<"angular rate"
, "rad/s", "estimated angular rate based on fused sensor measurements"
> angular_rate;
vec3_message< "integral feedback"
, float, -20.0f, 20.0f
, "integral feedback through the complementary filter"
> integral_feedback;
toggle<"stationary"
, "boolean indication of whether the sensor is stationary (1) or movement is detected (0)"
> stationary;
};
} }
static constexpr float rad_per_deg
Definition sygsp-mimu_units.hpp:40
static constexpr float mss_per_g
Definition sygsp-mimu_units.hpp:28
#pragma once
#include <array>
#include "sygac-mimu.hpp"
#include "sygsp-complementary_mimu_fusion_endpoints.hpp"
namespace sygaldry { namespace sygsp {
, ComplementaryMimuFusionOutputs& out
);
, const std::array<float, 3>& accl, bool accl_updated
, const std::array<float, 3>& magn, bool magn_updated
, const unsigned long elapsed
, const ComplementaryMimuFusionInputs& in
, ComplementaryMimuFusionOutputs& out
);
template<MimuComponent Mimu>
struct ComplementaryMimuFusion
: name_<"Complementary MIMU Fusion Filter">
, description_<"Runtime calibration and sensor fusion using a complementary filter approach">
, author_<"Travis J. West">
, copyright_<"Copyright 2023 Sygaldry Contributors">
, license_<"SPDX-License-Identifier: MIT">
, version_<"0.0.0">
{
ComplementaryMimuFusionInputs inputs;
ComplementaryMimuFusionOutputs outputs;
{
}
void main(const Mimu& mimu)
{
, mimu.outputs.elapsed
, inputs, outputs);
}
};
} }
auto & magn_of(auto &mimu_data)
Access the magnetometer data of a presumed MIMU data structure.
Definition sygac-mimu.hpp:52
auto & accl_of(auto &mimu_data)
Access the accelerometer data of a presumed MIMU data structure.
Definition sygac-mimu.hpp:30
auto & gyro_of(auto &mimu_data)
Access the gyroscope data of a presumed MIMU data structure.
Definition sygac-mimu.hpp:42
void complementary_mimu_fusion(const std::array< float, 3 > &gyro, const std::array< float, 3 > &accl, bool accl_updated, const std::array< float, 3 > &magn, bool magn_updated, const unsigned long elapsed, const ComplementaryMimuFusionInputs &in, ComplementaryMimuFusionOutputs &out)
Definition sygsp-complementary_mimu_fusion.cpp:52
void complementary_mimu_fusion_init(ComplementaryMimuFusionInputs &in, ComplementaryMimuFusionOutputs &out)
Definition sygsp-complementary_mimu_fusion.cpp:45
void init()
Initialize the filter.
Definition sygsp-complementary_mimu_fusion.hpp:73
#include "sygsp-complementary_mimu_fusion.hpp"
#include <Eigen/Eigen>
#include "sygsp-mimu_units.hpp"
namespace {
typedef Eigen::Vector3f Vector;
typedef Eigen::Matrix3f Matrix;
typedef Eigen::Quaternionf Quaternion;
{
out.quaternion = {1,0,0,0};
out.conjugate = {1,0,0,0};
out.matrix = {1,0,0, 0,1,0, 0,0,1};
out.inverse = {1,0,0, 0,1,0, 0,0,1};
out.norm_of_gravity = 1;
out.g_count = 1;
out.sensor_accl = {0};
out.global_accl = {0};
out.accl_prev = {0};
out.jerk = {0};
out.gyro_bias = {0};
out.magn_max = {0};
out.magn_min = {0};
out.magn_bias = {0};
out.angular_rate = {0};
out.integral_feedback = {0};
out.stationary = false;
}
}
namespace sygaldry { namespace sygsp {
{
in.initialize();
in.calibrate_magnetometer = in.calibrate_magnetometer.init();
filter_state_init(out);
}
, const std::array<float, 3>& accl, bool accl_updated
, const std::array<float, 3>& magn, bool magn_updated
, const unsigned long elapsed
, const ComplementaryMimuFusionInputs& in
, ComplementaryMimuFusionOutputs& out
)
{
Eigen::Map<const Vector> omega{gyro.data()};
Eigen::Map<const Vector> v_a{accl.data()};
Vector v_m{magn.data()};
Vector w_mes = Vector::Zero();
Eigen::Map<Matrix> inverse{out.inverse.state.data()};
auto v_hat_a = inverse.col(2);
auto v_hat_m = inverse.col(0);
if (accl_updated)
{
Eigen::Map<Vector> v_anm1{out.accl_prev.state.data()};
Eigen::Map<Vector> v_dot_a{out.jerk.state.data()};
Vector v_avg_a = 0.5 * v_a + 0.5 * v_anm1;
v_dot_a = v_avg_a - v_anm1; out.jerk.updated = true;
out.stationary = v_dot_a.dot(v_dot_a) < in.movement_threshold;
v_anm1 = v_a; out.accl_prev.updated = true;
if (out.stationary && out.g_count < out.g_count.max())
{
out.norm_of_gravity += (v_a.norm() - out.norm_of_gravity) / out.g_count;
out.g_count += 1;
}
Eigen::Map<Vector> v_a_lin{out.sensor_accl.state.data()};
Eigen::Map<Vector> v_a_lin_gf{out.global_accl.state.data()};
v_a_lin =
mss_per_g * (v_a - v_hat_a * out.norm_of_gravity); out.sensor_accl.updated =
true;
v_a_lin_gf = inverse * v_a_lin; out.global_accl.updated = true;
w_mes += v_a.normalized().cross(v_hat_a) * in.k_a;
}
if (magn_updated)
{
if (in.calibrate_magnetometer)
{
Eigen::Map<Vector> v_m_maxima{out.magn_max.state.data()};
Eigen::Map<Vector> v_m_minima{out.magn_min.state.data()};
Eigen::Map<Vector> v_m_bias{out.magn_bias.state.data()};
Vector max = v_m_maxima.cwiseMax(v_m);
Vector min = v_m_minima.cwiseMin(v_m);
if ((v_m_maxima.array() != max.array()).any() || (v_m_minima.array() != min.array()).any())
{
v_m_maxima = max; out.magn_max.updated = true;
v_m_minima = min; out.magn_min.updated = true;
v_m_bias = 0.5 * (min + max); out.magn_bias.updated = true;
}
v_m = v_m - v_m_bias;
}
v_m = v_hat_a.cross(v_m);
v_m.normalize();
w_mes += v_m.cross(v_hat_m) * in.k_m;
}
Eigen::Map<Vector> omega_bias{out.gyro_bias.state.data()};
if (out.stationary)
{
omega_bias = (1.0 - in.gyro_alpha) * omega + in.gyro_alpha * omega_bias;
out.gyro_bias.updated = true;
}
Eigen::Map<Vector> omega_hat{out.angular_rate.state.data()};
omega_hat = omega - omega_bias;
if (w_mes.x() != 0 || w_mes.y() != 0 || w_mes.z() != 0)
{
Eigen::Map<Vector> b_hat{out.integral_feedback.state.data()};
if (in.k_I > 0.0)
{
b_hat += w_mes * (double)elapsed/1000000.0;
omega_hat += in.k_P * w_mes + in.k_I * b_hat;
}
else
{
b_hat.setZero();
omega_hat += in.k_P * w_mes;
}
out.integral_feedback.updated = true;
}
out.angular_rate.updated = true;
Eigen::Map<Quaternion> q{out.quaternion.state.data()};
auto q_dot = q * Quaternion(0, omega_hat.x(), omega_hat.y(), omega_hat.z());
q.coeffs() += 0.5 * q_dot.coeffs() * (double)elapsed/1000000.0;
q.normalize(); out.quaternion.updated = true;
Eigen::Map<Quaternion> conjugate{out.conjugate.state.data()};
Eigen::Map<Matrix> matrix{out.matrix.state.data()};
conjugate = q.conjugate(); out.conjugate.updated = true;
matrix = q.toRotationMatrix(); out.matrix.updated = true;
inverse = conjugate.toRotationMatrix(); out.inverse.updated = true;
}
} }
Outputs of the MIMU sensor fusion component.
Definition sygsp-complementary_mimu_fusion_endpoints.hpp:73
#include <catch2/catch_test_macros.hpp>
#include "sygsp-complementary_mimu_fusion.hpp"
using namespace sygaldry;
using namespace sygaldry::sygsp;
{tests}
# @#'CMakeLists.txt'
set(lib sygsp-complementary_mimu_fusion)
add_library(${lib} INTERFACE)
target_sources(${lib}
INTERFACE ${lib}.cpp
)
target_include_directories(${lib}
INTERFACE .
)
target_link_libraries(${lib}
INTERFACE sygah-endpoints
INTERFACE sygah-metadata
INTERFACE sygah-mimu
INTERFACE sygac-mimu
INTERFACE sygsp-mimu_units
INTERFACE Eigen3::Eigen
)
# TODO: write tests
# @/