Improve gyro zeroing
Instead of using a single large data chunk for zeroing, collect multiple
shorter samples that we then gradually accumulate.
Change-Id: I5496a7c47a703764fcd0f5b1498ef080e9b30fe9
diff --git a/frc971/control_loops/drivetrain/drivetrain.cc b/frc971/control_loops/drivetrain/drivetrain.cc
index 094a947..1afc32d 100644
--- a/frc971/control_loops/drivetrain/drivetrain.cc
+++ b/frc971/control_loops/drivetrain/drivetrain.cc
@@ -147,6 +147,9 @@
while (imu_values_fetcher_.FetchNext()) {
imu_zeroer_.ProcessMeasurement(*imu_values_fetcher_);
last_gyro_time_ = monotonic_now;
+ if (!imu_zeroer_.Zeroed()) {
+ continue;
+ }
aos::monotonic_clock::time_point reading_time(std::chrono::nanoseconds(
imu_values_fetcher_->monotonic_timestamp_ns()));
if (last_imu_update_ == aos::monotonic_clock::min_time) {
diff --git a/frc971/zeroing/BUILD b/frc971/zeroing/BUILD
index a2b908b..75d610a 100644
--- a/frc971/zeroing/BUILD
+++ b/frc971/zeroing/BUILD
@@ -35,6 +35,8 @@
],
deps = [
":averager",
+ "//frc971/control_loops:control_loops_fbs",
+ "//frc971/control_loops/drivetrain:drivetrain_status_fbs",
"//frc971/wpilib:imu_fbs",
"@com_github_google_glog//:glog",
"@org_tuxfamily_eigen//:eigen",
diff --git a/frc971/zeroing/averager.h b/frc971/zeroing/averager.h
index b6ff09d..23a1b06 100644
--- a/frc971/zeroing/averager.h
+++ b/frc971/zeroing/averager.h
@@ -19,6 +19,11 @@
class Averager {
public:
typedef Eigen::Matrix<Scalar, rows_per_sample, 1> Vector;
+ Averager() {
+ for (Vector &datum : data_) {
+ datum.setZero();
+ }
+ }
// Adds one data point to the set of data points to be averaged.
// If more than "num_samples" samples are added, they will start overwriting
// the oldest ones.
diff --git a/frc971/zeroing/imu_zeroer.cc b/frc971/zeroing/imu_zeroer.cc
index 25bf6f6..b13b43d 100644
--- a/frc971/zeroing/imu_zeroer.cc
+++ b/frc971/zeroing/imu_zeroer.cc
@@ -4,22 +4,30 @@
ImuZeroer::ImuZeroer() {
gyro_average_.setZero();
+ accel_average_.setZero();
last_gyro_sample_.setZero();
last_accel_sample_.setZero();
}
-bool ImuZeroer::Zeroed() const { return zeroed_ || Faulted(); }
+bool ImuZeroer::Zeroed() const {
+ return num_zeroes_ > kRequiredZeroPoints || Faulted();
+}
bool ImuZeroer::Faulted() const { return faulted_; }
Eigen::Vector3d ImuZeroer::ZeroedGyro() const {
return last_gyro_sample_ - gyro_average_;
}
-Eigen::Vector3d ImuZeroer::ZeroedAccel() const { return last_accel_sample_; }
+Eigen::Vector3d ImuZeroer::ZeroedAccel() const {
+ return last_accel_sample_ - accel_average_;
+}
Eigen::Vector3d ImuZeroer::GyroOffset() const { return gyro_average_; }
bool ImuZeroer::GyroZeroReady() const {
- return gyro_averager_.full() && gyro_averager_.GetRange() < kGyroMaxVariation;
+ return gyro_averager_.full() &&
+ gyro_averager_.GetRange() < kGyroMaxVariation &&
+ (last_gyro_sample_.lpNorm<Eigen::Infinity>() <
+ kGyroMaxZeroingMagnitude);
}
bool ImuZeroer::AccelZeroReady() const {
@@ -34,19 +42,45 @@
values.accelerometer_z();
accel_averager_.AddData(last_accel_sample_);
if (GyroZeroReady() && AccelZeroReady()) {
- if (!zeroed_) {
- zeroed_ = true;
- gyro_average_ = gyro_averager_.GetAverage();
- } else {
- // If we got a new zero and it is substantially different from the
- // original zero, fault.
- if ((gyro_averager_.GetAverage() - gyro_average_).norm() >
- kGyroFaultVariation) {
- faulted_ = true;
+ ++good_iters_;
+ if (good_iters_ > kSamplesToAverage / 4) {
+ const Eigen::Vector3d current_gyro_average = gyro_averager_.GetAverage();
+ constexpr double kAverageUpdateWeight = 0.05;
+ if (num_zeroes_ > 0) {
+ gyro_average_ +=
+ (current_gyro_average - gyro_average_) * kAverageUpdateWeight;
+ } else {
+ gyro_average_ = current_gyro_average;
}
+ if (num_zeroes_ > 0) {
+ // If we got a new zero and it is substantially different from the
+ // original zero, fault.
+ if ((current_gyro_average - gyro_average_).norm() >
+ kGyroFaultVariation) {
+ faulted_ = true;
+ }
+ }
+ ++num_zeroes_;
+ gyro_averager_.Reset();
}
- gyro_averager_.Reset();
+ } else {
+ good_iters_ = 0;
}
}
+flatbuffers::Offset<control_loops::drivetrain::ImuZeroerState>
+ImuZeroer::PopulateStatus(flatbuffers::FlatBufferBuilder *fbb) const {
+ control_loops::drivetrain::ImuZeroerState::Builder builder(*fbb);
+
+ builder.add_zeroed(Zeroed());
+ builder.add_faulted(Faulted());
+ builder.add_number_of_zeroes(num_zeroes_);
+
+ builder.add_gyro_x_average(GyroOffset().x());
+ builder.add_gyro_y_average(GyroOffset().y());
+ builder.add_gyro_z_average(GyroOffset().z());
+
+ return builder.Finish();
+}
+
} // namespace frc971::zeroing
diff --git a/frc971/zeroing/imu_zeroer.h b/frc971/zeroing/imu_zeroer.h
index 2662934..b55c6dd 100644
--- a/frc971/zeroing/imu_zeroer.h
+++ b/frc971/zeroing/imu_zeroer.h
@@ -1,6 +1,7 @@
#ifndef FRC971_ZEROING_IMU_ZEROER_H_
#define FRC971_ZEROING_IMU_ZEROER_H_
+#include "frc971/control_loops/drivetrain/drivetrain_status_generated.h"
#include "frc971/wpilib/imu_generated.h"
#include "frc971/zeroing/averager.h"
@@ -15,7 +16,8 @@
// constant number of samples...
// TODO(james): Run average and GetRange calculations over every sample on
// every timestep, to provide consistent timing.
- static constexpr size_t kSamplesToAverage = 1000.0;
+ static constexpr size_t kSamplesToAverage = 1000;
+ static constexpr size_t kRequiredZeroPoints = 10;
ImuZeroer();
bool Zeroed() const;
@@ -24,14 +26,22 @@
Eigen::Vector3d GyroOffset() const;
Eigen::Vector3d ZeroedGyro() const;
Eigen::Vector3d ZeroedAccel() const;
+
+ flatbuffers::Offset<control_loops::drivetrain::ImuZeroerState> PopulateStatus(
+ flatbuffers::FlatBufferBuilder *fbb) const;
+
private:
// Max variation (difference between the maximum and minimum value) in a
// kSamplesToAverage range before we allow using the samples for zeroing.
// These values are currently based on looking at results from the ADIS16448.
- static constexpr double kGyroMaxVariation = 0.02; // rad / sec
+ static constexpr double kGyroMaxVariation = 0.02; // rad / sec
+ // Maximum magnitude we allow the gyro zero to have--this is used to prevent
+ // us from zeroing the gyro if we just happen to be spinning at a very
+ // consistent non-zero rate. Currently this is only plausible in simulation.
+ static constexpr double kGyroMaxZeroingMagnitude = 0.1; // rad / sec
// Max variation in the range before we consider the accelerometer readings to
// be steady.
- static constexpr double kAccelMaxVariation = 0.02; // g's
+ static constexpr double kAccelMaxVariation = 0.05; // g's
// If we ever are able to rezero and get a zero that is more than
// kGyroFaultVariation away from the original zeroing, fault.
static constexpr double kGyroFaultVariation = 0.005; // rad / sec
@@ -46,12 +56,13 @@
Averager<double, kSamplesToAverage, 3> accel_averager_;
// The average zero position of the gyro.
Eigen::Vector3d gyro_average_;
+ Eigen::Vector3d accel_average_;
Eigen::Vector3d last_gyro_sample_;
Eigen::Vector3d last_accel_sample_;
- // Whether we have currently zeroed yet.
- bool zeroed_ = false;
// Whether the zeroing has faulted at any point thus far.
bool faulted_ = false;
+ size_t good_iters_ = 0;
+ size_t num_zeroes_ = 0;
};
} // namespace frc971::zeroing
diff --git a/frc971/zeroing/imu_zeroer_test.cc b/frc971/zeroing/imu_zeroer_test.cc
index 9ec52da..7a74413 100644
--- a/frc971/zeroing/imu_zeroer_test.cc
+++ b/frc971/zeroing/imu_zeroer_test.cc
@@ -4,6 +4,9 @@
namespace frc971::zeroing {
+static constexpr int kMinSamplesToZero =
+ 2 * ImuZeroer::kSamplesToAverage * ImuZeroer::kRequiredZeroPoints;
+
aos::FlatbufferDetachedBuffer<IMUValues> MakeMeasurement(
const Eigen::Vector3d &gyro, const Eigen::Vector3d &accel) {
flatbuffers::FlatBufferBuilder fbb;
@@ -29,13 +32,14 @@
ASSERT_EQ(0.0, zeroer.ZeroedAccel().norm());
// A measurement before we are zeroed should just result in the measurement
// being passed through without modification.
- zeroer.ProcessMeasurement(MakeMeasurement({1, 2, 3}, {4, 5, 6}).message());
+ zeroer.ProcessMeasurement(
+ MakeMeasurement({0.01, 0.02, 0.03}, {4, 5, 6}).message());
ASSERT_FALSE(zeroer.Zeroed());
ASSERT_FALSE(zeroer.Faulted());
ASSERT_EQ(0.0, zeroer.GyroOffset().norm());
- ASSERT_EQ(1.0, zeroer.ZeroedGyro().x());
- ASSERT_EQ(2.0, zeroer.ZeroedGyro().y());
- ASSERT_EQ(3.0, zeroer.ZeroedGyro().z());
+ ASSERT_FLOAT_EQ(0.01, zeroer.ZeroedGyro().x());
+ ASSERT_FLOAT_EQ(0.02, zeroer.ZeroedGyro().y());
+ ASSERT_FLOAT_EQ(0.03, zeroer.ZeroedGyro().z());
ASSERT_EQ(4.0, zeroer.ZeroedAccel().x());
ASSERT_EQ(5.0, zeroer.ZeroedAccel().y());
ASSERT_EQ(6.0, zeroer.ZeroedAccel().z());
@@ -45,16 +49,16 @@
TEST(ImuZeroerTest, ZeroOnConstantData) {
ImuZeroer zeroer;
ASSERT_FALSE(zeroer.Zeroed());
- for (size_t ii = 0; ii < ImuZeroer::kSamplesToAverage; ++ii) {
- ASSERT_FALSE(zeroer.Zeroed());
- zeroer.ProcessMeasurement(MakeMeasurement({1, 2, 3}, {4, 5, 6}).message());
+ for (size_t ii = 0; ii < kMinSamplesToZero; ++ii) {
+ zeroer.ProcessMeasurement(
+ MakeMeasurement({0.01, 0.02, 0.03}, {4, 5, 6}).message());
}
ASSERT_TRUE(zeroer.Zeroed());
ASSERT_FALSE(zeroer.Faulted());
// Gyro should be zeroed to {1, 2, 3}.
- ASSERT_EQ(1.0, zeroer.GyroOffset().x());
- ASSERT_EQ(2.0, zeroer.GyroOffset().y());
- ASSERT_EQ(3.0, zeroer.GyroOffset().z());
+ ASSERT_FLOAT_EQ(0.01, zeroer.GyroOffset().x());
+ ASSERT_FLOAT_EQ(0.02, zeroer.GyroOffset().y());
+ ASSERT_FLOAT_EQ(0.03, zeroer.GyroOffset().z());
ASSERT_EQ(0.0, zeroer.ZeroedGyro().x());
ASSERT_EQ(0.0, zeroer.ZeroedGyro().y());
ASSERT_EQ(0.0, zeroer.ZeroedGyro().z());
@@ -64,11 +68,25 @@
ASSERT_EQ(6.0, zeroer.ZeroedAccel().z());
// If we get another measurement offset by {1, 1, 1} we should read the result
// as {1, 1, 1}.
- zeroer.ProcessMeasurement(MakeMeasurement({2, 3, 4}, {0, 0, 0}).message());
+ zeroer.ProcessMeasurement(
+ MakeMeasurement({0.02, 0.03, 0.04}, {0, 0, 0}).message());
ASSERT_FALSE(zeroer.Faulted());
- ASSERT_EQ(1.0, zeroer.ZeroedGyro().x());
- ASSERT_EQ(1.0, zeroer.ZeroedGyro().y());
- ASSERT_EQ(1.0, zeroer.ZeroedGyro().z());
+ ASSERT_FLOAT_EQ(0.01, zeroer.ZeroedGyro().x());
+ ASSERT_FLOAT_EQ(0.01, zeroer.ZeroedGyro().y());
+ ASSERT_FLOAT_EQ(0.01, zeroer.ZeroedGyro().z());
+}
+
+// Tests that we do not zero if the gyro is producing particularly high
+// magnitude results.
+TEST(ImuZeroerTest, NoZeroOnHighMagnitudeGyro) {
+ ImuZeroer zeroer;
+ ASSERT_FALSE(zeroer.Zeroed());
+ for (size_t ii = 0; ii < kMinSamplesToZero; ++ii) {
+ zeroer.ProcessMeasurement(
+ MakeMeasurement({0.1, 0.2, 0.3}, {4, 5, 6}).message());
+ ASSERT_FALSE(zeroer.Zeroed());
+ }
+ ASSERT_FALSE(zeroer.Faulted());
}
// Tests that we tolerate small amounts of noise in the incoming data and can
@@ -76,29 +94,27 @@
TEST(ImuZeroerTest, ZeroOnLowNoiseData) {
ImuZeroer zeroer;
ASSERT_FALSE(zeroer.Zeroed());
- for (size_t ii = 0; ii < ImuZeroer::kSamplesToAverage; ++ii) {
- ASSERT_FALSE(zeroer.Zeroed());
+ for (size_t ii = 0; ii < kMinSamplesToZero; ++ii) {
const double offset =
- (static_cast<double>(ii) / (ImuZeroer::kSamplesToAverage - 1) - 0.5) *
- 0.01;
+ (static_cast<double>(ii) / (kMinSamplesToZero - 1) - 0.5) * 0.001;
zeroer.ProcessMeasurement(
- MakeMeasurement({1 + offset, 2 + offset, 3 + offset},
+ MakeMeasurement({0.01 + offset, 0.02 + offset, 0.03 + offset},
{4 + offset, 5 + offset, 6 + offset})
.message());
}
ASSERT_TRUE(zeroer.Zeroed());
ASSERT_FALSE(zeroer.Faulted());
- // Gyro should be zeroed to {1, 2, 3}.
- ASSERT_NEAR(1.0, zeroer.GyroOffset().x(), 1e-8);
- ASSERT_NEAR(2.0, zeroer.GyroOffset().y(), 1e-8);
- ASSERT_NEAR(3.0, zeroer.GyroOffset().z(), 1e-8);
- // If we get another measurement offset by {1, 1, 1} we should read the result
- // as {1, 1, 1}.
- zeroer.ProcessMeasurement(MakeMeasurement({2, 3, 4}, {0, 0, 0}).message());
+ ASSERT_NEAR(0.01, zeroer.GyroOffset().x(), 1e-3);
+ ASSERT_NEAR(0.02, zeroer.GyroOffset().y(), 1e-3);
+ ASSERT_NEAR(0.03, zeroer.GyroOffset().z(), 1e-3);
+ // If we get another measurement offset by {0.01, 0.01, 0.01} we should read
+ // the result as {0.01, 0.01, 0.01}.
+ zeroer.ProcessMeasurement(
+ MakeMeasurement({0.02, 0.03, 0.04}, {0, 0, 0}).message());
ASSERT_FALSE(zeroer.Faulted());
- ASSERT_NEAR(1.0, zeroer.ZeroedGyro().x(), 1e-8);
- ASSERT_NEAR(1.0, zeroer.ZeroedGyro().y(), 1e-8);
- ASSERT_NEAR(1.0, zeroer.ZeroedGyro().z(), 1e-8);
+ ASSERT_NEAR(0.01, zeroer.ZeroedGyro().x(), 1e-3);
+ ASSERT_NEAR(0.01, zeroer.ZeroedGyro().y(), 1e-3);
+ ASSERT_NEAR(0.01, zeroer.ZeroedGyro().z(), 1e-3);
ASSERT_EQ(0.0, zeroer.ZeroedAccel().x());
ASSERT_EQ(0.0, zeroer.ZeroedAccel().y());
ASSERT_EQ(0.0, zeroer.ZeroedAccel().z());
@@ -108,13 +124,12 @@
TEST(ImuZeroerTest, NoZeroOnHighNoiseData) {
ImuZeroer zeroer;
ASSERT_FALSE(zeroer.Zeroed());
- for (size_t ii = 0; ii < ImuZeroer::kSamplesToAverage; ++ii) {
+ for (size_t ii = 0; ii < kMinSamplesToZero; ++ii) {
ASSERT_FALSE(zeroer.Zeroed());
const double offset =
- (static_cast<double>(ii) / (ImuZeroer::kSamplesToAverage - 1) - 0.5) *
- 1.0;
+ (static_cast<double>(ii) / (kMinSamplesToZero - 1) - 0.5) * 1.0;
zeroer.ProcessMeasurement(
- MakeMeasurement({1 + offset, 2 + offset, 3 + offset},
+ MakeMeasurement({0.01 + offset, 0.02 + offset, 0.03 + offset},
{4 + offset, 5 + offset, 6 + offset})
.message());
}
@@ -127,15 +142,16 @@
TEST(ImuZeroerTest, FaultOnNewZero) {
ImuZeroer zeroer;
ASSERT_FALSE(zeroer.Zeroed());
- for (size_t ii = 0; ii < ImuZeroer::kSamplesToAverage; ++ii) {
- ASSERT_FALSE(zeroer.Zeroed());
- zeroer.ProcessMeasurement(MakeMeasurement({1, 2, 3}, {4, 5, 6}).message());
+ for (size_t ii = 0; ii < kMinSamplesToZero; ++ii) {
+ zeroer.ProcessMeasurement(
+ MakeMeasurement({0.01, 0.02, 0.03}, {4, 5, 6}).message());
}
ASSERT_TRUE(zeroer.Zeroed());
- for (size_t ii = 0; ii < ImuZeroer::kSamplesToAverage; ++ii) {
- ASSERT_FALSE(zeroer.Faulted())
- << "We should not fault until we complete a second cycle of zeroing.";
- zeroer.ProcessMeasurement(MakeMeasurement({1, 5, 3}, {4, 5, 6}).message());
+ ASSERT_FALSE(zeroer.Faulted())
+ << "We should not fault until we complete a second cycle of zeroing.";
+ for (size_t ii = 0; ii < kMinSamplesToZero; ++ii) {
+ zeroer.ProcessMeasurement(
+ MakeMeasurement({0.01, 0.05, 0.03}, {4, 5, 6}).message());
}
ASSERT_TRUE(zeroer.Faulted());
}
@@ -144,14 +160,14 @@
TEST(ImuZeroerTest, NoFaultOnSimilarZero) {
ImuZeroer zeroer;
ASSERT_FALSE(zeroer.Zeroed());
- for (size_t ii = 0; ii < ImuZeroer::kSamplesToAverage; ++ii) {
- ASSERT_FALSE(zeroer.Zeroed());
- zeroer.ProcessMeasurement(MakeMeasurement({1, 2, 3}, {4, 5, 6}).message());
+ for (size_t ii = 0; ii < kMinSamplesToZero; ++ii) {
+ zeroer.ProcessMeasurement(
+ MakeMeasurement({0.01, 0.02, 0.03}, {4, 5, 6}).message());
}
ASSERT_TRUE(zeroer.Zeroed());
- for (size_t ii = 0; ii < ImuZeroer::kSamplesToAverage; ++ii) {
+ for (size_t ii = 0; ii < kMinSamplesToZero; ++ii) {
zeroer.ProcessMeasurement(
- MakeMeasurement({1, 2.0001, 3}, {4, 5, 6}).message());
+ MakeMeasurement({0.01, 0.020001, 0.03}, {4, 5, 6}).message());
}
ASSERT_FALSE(zeroer.Faulted());
}