EKF: Add support for use of GPS heading data.

Heading data is assumed to be from a  dual antenna array at a specified yaw angle offset in body frame, but with the heading data already corrected for antenna offset. The offset is required to apply the correct compensation for combined rotations and to determine when the yaw observation has become badly conditioned.
This commit is contained in:
Paul Riseborough
2018-09-19 06:32:43 +10:00
committed by Paul Riseborough
parent af60e6bbc8
commit 91f886cb5e
8 changed files with 499 additions and 1 deletions
+433
View File
@@ -0,0 +1,433 @@
/****************************************************************************
*
* Copyright (c) 2018 Estimation and Control Library (ECL). All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
* 3. Neither the name ECL nor the names of its contributors may be
* used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
* OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
* AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
* ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
****************************************************************************/
/**
* @file gps_yaw_fusion.cpp
* Definition of functions required to use yaw obtained from GPS dual antenna measurements.
*
* @author Paul Riseborough <p_riseborough@live.com.au>
*
*/
#include "ekf.h"
#include <ecl.h>
#include <mathlib/mathlib.h>
#include <cstdlib>
void Ekf::fuseGpsAntYaw()
{
// assign intermediate state variables
float q0 = _state.quat_nominal(0);
float q1 = _state.quat_nominal(1);
float q2 = _state.quat_nominal(2);
float q3 = _state.quat_nominal(3);
float R_YAW = 1.0f;
float predicted_hdg;
float H_YAW[4];
float measured_hdg;
// check if data has been set to NAN indicating no measurement
if (isfinite(_gps_sample_delayed.yaw)) {
// calculate the observed yaw angle of antenna array, converting a from body to antenna yaw measurement
measured_hdg = _gps_sample_delayed.yaw + _gps_yaw_offset;
// define the predicted antenna array vector and rotate into earth frame
Vector3f ant_vec_bf = {cosf(_gps_yaw_offset), sinf(_gps_yaw_offset), 0.0f};
Vector3f ant_vec_ef = _R_to_earth * ant_vec_bf;
// check if antenna array vector is within 30 degrees of vertical and therefore unable to provide a reliable heading
if (fabsf(ant_vec_ef(2)) > cosf(math::radians(30.0f))) {
return;
}
// calculate predicted antenna yaw angle
predicted_hdg = atan2f(ant_vec_ef(1),ant_vec_ef(0));
// calculate observation jacobian
float t2 = sin(_gps_yaw_offset);
float t3 = cos(_gps_yaw_offset);
float t4 = q0*q3*2.0f;
float t5 = q0*q0;
float t6 = q1*q1;
float t7 = q2*q2;
float t8 = q3*q3;
float t9 = q1*q2*2.0f;
float t10 = t5+t6-t7-t8;
float t11 = t3*t10;
float t12 = t4+t9;
float t13 = t3*t12;
float t14 = t5-t6+t7-t8;
float t15 = t2*t14;
float t16 = t13+t15;
float t17 = t4-t9;
float t19 = t2*t17;
float t20 = t11-t19;
float t18 = (t20*t20);
if (t18 < 1e-6f) {
return;
}
t18 = 1.0f / t18;
float t21 = t16*t16;
float t22 = sq(t11-t19);
if (t22 < 1e-6f) {
return;
}
t22 = 1.0f/t22;
float t23 = q1*t3*2.0f;
float t24 = q2*t2*2.0f;
float t25 = t23+t24;
float t26 = 1.0f/t20;
float t27 = q1*t2*2.0f;
float t28 = t21*t22;
float t29 = t28+1.0f;
if (fabsf(t29) < 1e-6f) {
return;
}
float t30 = 1.0f/t29;
float t31 = q0*t3*2.0f;
float t32 = t31-q3*t2*2.0f;
float t33 = q3*t3*2.0f;
float t34 = q0*t2*2.0f;
float t35 = t33+t34;
H_YAW[0] = (t35/(t11-t2*(t4-q1*q2*2.0f))-t16*t18*t32)/(t18*t21+1.0f);
H_YAW[1] = -t30*(t26*(t27-q2*t3*2.0f)+t16*t22*t25);
H_YAW[2] = t30*(t25*t26-t16*t22*(t27-q2*t3*2.0f));
H_YAW[3] = t30*(t26*t32+t16*t22*t35);
// using magnetic heading tuning parameter
R_YAW = sq(fmaxf(_params.mag_heading_noise, 1.0e-2f));
} else {
// there is nothing to fuse
return;
}
// wrap the heading to the interval between +-pi
measured_hdg = wrap_pi(measured_hdg);
// calculate the innovation and define the innovaton gate
float innov_gate = math::max(_params.heading_innov_gate, 1.0f);
_heading_innov = predicted_hdg - measured_hdg;
// wrap the innovation to the interval between +-pi
_heading_innov = wrap_pi(_heading_innov);
// Calculate innovation variance and Kalman gains, taking advantage of the fact that only the first 3 elements in H are non zero
// calculate the innovaton variance
float PH[4];
_heading_innov_var = R_YAW;
for (unsigned row = 0; row <= 3; row++) {
PH[row] = 0.0f;
for (uint8_t col = 0; col <= 3; col++) {
PH[row] += P[row][col] * H_YAW[col];
}
_heading_innov_var += H_YAW[row] * PH[row];
}
float heading_innov_var_inv;
// check if the innovation variance calculation is badly conditioned
if (_heading_innov_var >= R_YAW) {
// the innovation variance contribution from the state covariances is not negative, no fault
_fault_status.flags.bad_mag_hdg = false;
heading_innov_var_inv = 1.0f / _heading_innov_var;
} else {
// the innovation variance contribution from the state covariances is negative which means the covariance matrix is badly conditioned
_fault_status.flags.bad_mag_hdg = true;
// we reinitialise the covariance matrix and abort this fusion step
initialiseCovariance();
ECL_ERR("EKF GPS yaw fusion numerical error - covariance reset");
return;
}
// calculate the Kalman gains
// only calculate gains for states we are using
float Kfusion[_k_num_states] = {};
for (uint8_t row = 0; row <= 15; row++) {
Kfusion[row] = 0.0f;
for (uint8_t col = 0; col <= 3; col++) {
Kfusion[row] += P[row][col] * H_YAW[col];
}
Kfusion[row] *= heading_innov_var_inv;
}
if (_control_status.flags.wind) {
for (uint8_t row = 22; row <= 23; row++) {
Kfusion[row] = 0.0f;
for (uint8_t col = 0; col <= 3; col++) {
Kfusion[row] += P[row][col] * H_YAW[col];
}
Kfusion[row] *= heading_innov_var_inv;
}
}
// innovation test ratio
_yaw_test_ratio = sq(_heading_innov) / (sq(innov_gate) * _heading_innov_var);
// we are no longer using 3-axis fusion so set the reported test levels to zero
memset(_mag_test_ratio, 0, sizeof(_mag_test_ratio));
// set the magnetometer unhealthy if the test fails
if (_yaw_test_ratio > 1.0f) {
_innov_check_fail_status.flags.reject_yaw = true;
// if we are in air we don't want to fuse the measurement
// we allow to use it when on the ground because the large innovation could be caused
// by interference or a large initial gyro bias
if (_control_status.flags.in_air) {
return;
} else {
// constrain the innovation to the maximum set by the gate
float gate_limit = sqrtf((sq(innov_gate) * _heading_innov_var));
_heading_innov = math::constrain(_heading_innov, -gate_limit, gate_limit);
}
} else {
_innov_check_fail_status.flags.reject_yaw = false;
}
// apply covariance correction via P_new = (I -K*H)*P
// first calculate expression for KHP
// then calculate P - KHP
float KHP[_k_num_states][_k_num_states];
float KH[4];
for (unsigned row = 0; row < _k_num_states; row++) {
KH[0] = Kfusion[row] * H_YAW[0];
KH[1] = Kfusion[row] * H_YAW[1];
KH[2] = Kfusion[row] * H_YAW[2];
KH[3] = Kfusion[row] * H_YAW[3];
for (unsigned column = 0; column < _k_num_states; column++) {
float tmp = KH[0] * P[0][column];
tmp += KH[1] * P[1][column];
tmp += KH[2] * P[2][column];
tmp += KH[3] * P[3][column];
KHP[row][column] = tmp;
}
}
// if the covariance correction will result in a negative variance, then
// the covariance marix is unhealthy and must be corrected
bool healthy = true;
_fault_status.flags.bad_mag_hdg = false;
for (int i = 0; i < _k_num_states; i++) {
if (P[i][i] < KHP[i][i]) {
// zero rows and columns
zeroRows(P, i, i);
zeroCols(P, i, i);
//flag as unhealthy
healthy = false;
// update individual measurement health status
_fault_status.flags.bad_mag_hdg = true;
}
}
// only apply covariance and state corrrections if healthy
if (healthy) {
// apply the covariance corrections
for (unsigned row = 0; row < _k_num_states; row++) {
for (unsigned column = 0; column < _k_num_states; column++) {
P[row][column] = P[row][column] - KHP[row][column];
}
}
// correct the covariance marix for gross errors
fixCovarianceErrors();
// apply the state corrections
fuse(Kfusion, _heading_innov);
}
}
bool Ekf::resetGpsAntYaw()
{
// check if data has been set to NAN indicating no measurement
if (isfinite(_gps_sample_delayed.yaw)) {
// define the predicted antenna array vector and rotate into earth frame
Vector3f ant_vec_bf = {cosf(_gps_yaw_offset), sinf(_gps_yaw_offset), 0.0f};
Vector3f ant_vec_ef = _R_to_earth * ant_vec_bf;
// check if antenna array vector is within 30 degrees of vertical and therefore unable to provide a reliable heading
if (fabsf(ant_vec_ef(2)) > cosf(math::radians(30.0f))) {
return false;
}
float predicted_yaw = atan2f(ant_vec_ef(1),ant_vec_ef(0));
// get measurement and correct for antenna array yaw offset
float measured_yaw = _gps_sample_delayed.yaw + _gps_yaw_offset;
// caclulate the amount the yaw needs to be rotated by
float yaw_delta = wrap_pi(measured_yaw - predicted_yaw);
// save a copy of the quaternion state for later use in calculating the amount of reset change
Quatf quat_before_reset = _state.quat_nominal;
Quatf quat_after_reset = _state.quat_nominal;
// obtain the yaw angle using the best conditioned from either a Tait-Bryan 321 or 312 sequence
// to avoid gimbal lock
if (fabsf(_R_to_earth(2, 0)) < fabsf(_R_to_earth(2, 1))) {
// get the roll, pitch, yaw estimates from the quaternion states using a 321 Tait-Bryan rotation sequence
Quatf q_init(_state.quat_nominal);
Eulerf euler_init(q_init);
// correct the yaw angle
euler_init(2) += yaw_delta;
euler_init(2) = wrap_pi(euler_init(2));
// update the quaternions
quat_after_reset = Quatf(euler_init);
} else {
// Calculate the 312 Tait-Bryan sequence euler angles that rotate from earth to body frame
// PX4 math library does not support this so are using equations from
// http://www.atacolorado.com/eulersequences.doc
Vector3f euler312;
euler312(0) = atan2f(-_R_to_earth(0, 1), _R_to_earth(1, 1)); // first rotation (yaw)
euler312(1) = asinf(_R_to_earth(2, 1)); // second rotation (roll)
euler312(2) = atan2f(-_R_to_earth(2, 0), _R_to_earth(2, 2)); // third rotation (pitch)
// correct the yaw angle
euler312(0) += yaw_delta;
euler312(0) = wrap_pi(euler312(0));
// Calculate the body to earth frame rotation matrix from the corrected euler angles
float c2 = cosf(euler312(2));
float s2 = sinf(euler312(2));
float s1 = sinf(euler312(1));
float c1 = cosf(euler312(1));
float s0 = sinf(euler312(0));
float c0 = cosf(euler312(0));
Dcmf R_to_earth;
R_to_earth(0, 0) = c0 * c2 - s0 * s1 * s2;
R_to_earth(1, 1) = c0 * c1;
R_to_earth(2, 2) = c2 * c1;
R_to_earth(0, 1) = -c1 * s0;
R_to_earth(0, 2) = s2 * c0 + c2 * s1 * s0;
R_to_earth(1, 0) = c2 * s0 + s2 * s1 * c0;
R_to_earth(1, 2) = s0 * s2 - s1 * c0 * c2;
R_to_earth(2, 0) = -s2 * c1;
R_to_earth(2, 1) = s1;
// update the quaternions
quat_after_reset = Quatf(R_to_earth);
}
// calculate the amount that the quaternion has changed by
Quatf q_error = quat_before_reset.inversed() * _state.quat_nominal;
q_error.normalize();
// convert the quaternion delta to a delta angle
Vector3f delta_ang_error;
float scalar;
if (q_error(0) >= 0.0f) {
scalar = -2.0f;
} else {
scalar = 2.0f;
}
delta_ang_error(0) = scalar * q_error(1);
delta_ang_error(1) = scalar * q_error(2);
delta_ang_error(2) = scalar * q_error(3);
// calculate the variance for the rotation estimate expressed as a rotation vector
// this will be used later to reset the quaternion state covariances
Vector3f angle_err_var_vec = calcRotVecVariances();
// update the quaternion state estimates and corresponding covariances only if the change in angle has been large or the yaw is not yet aligned
if (delta_ang_error.norm() > math::radians(15.0f) || !_control_status.flags.yaw_align) {
// update quaternion states
_state.quat_nominal = quat_after_reset;
// record the state change
_state_reset_status.quat_change = q_error;
// update transformation matrix from body to world frame using the current estimate
_R_to_earth = quat_to_invrotmat(_state.quat_nominal);
// reset the rotation from the EV to EKF frame of reference if it is being used
if ((_params.fusion_mode & MASK_ROTATE_EV) && (_params.fusion_mode & MASK_USE_EVPOS)) {
resetExtVisRotMat();
}
// update the yaw angle variance using the variance of the measurement
angle_err_var_vec(2) = sq(fmaxf(_params.mag_heading_noise, 1.0e-2f));
// reset the quaternion covariances using the rotation vector variances
initialiseQuatCovariances(angle_err_var_vec);
// add the reset amount to the output observer buffered data
for (uint8_t i = 0; i < _output_buffer.get_length(); i++) {
// Note q1 *= q2 is equivalent to q1 = q2 * q1
_output_buffer[i].quat_nominal *= _state_reset_status.quat_change;
}
// apply the change in attitude quaternion to our newest quaternion estimate
// which was already taken out from the output buffer
_output_new.quat_nominal = _state_reset_status.quat_change * _output_new.quat_nominal;
// capture the reset event
_state_reset_status.quat_counter++;
}
return true;
}
return false;
}