Skip to content

Commit 9d341b7

Browse files
committed
Version 2.3.0
Extended Kalman filter added. A number of bug fixes.
1 parent 0111a12 commit 9d341b7

File tree

15 files changed

+798
-587
lines changed

15 files changed

+798
-587
lines changed

LICENSE.md

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,6 @@ The above copyright notice and this permission notice shall be included in all c
1414

1515
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.
1616

17-
/******************************************************************
18-
GNU License applicable to Kalman Filter.
19-
20-
******************************************************************/
21-
22-
Copyright (C) 2012 Kristian Lauszus, TKJ Electronics. All rights reserved.
23-
24-
This software may be distributed and modified under the terms of the GNU
25-
General Public License version 2 (GPL2) as published by the Free Software
26-
Foundation and appearing in the file GPL2.TXT included in the packaging of
27-
this file. Please note that GPL2 Section 2[b] requires that all works based
28-
on this software must also be made publicly available under the terms of
29-
the GPL2 ("Copyleft").
30-
3117
/******************************************************************
3218
GNU License applicable to code associated with Madgwick and
3319
Mahony Filters.

README.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
![version](https://img.shields.io/github/v/tag/Reefwing-Software/Reefwing-AHRS) ![license](https://img.shields.io/badge/license-MIT-green) ![release](https://img.shields.io/github/release-date/Reefwing-Software/Reefwing-AHRS?color="red") ![open source](https://badgen.net/badge/open/source/blue?icon=github)
1+
[![arduino-library-badge](https://www.ardu-badge.com/badge/ReefwingAHRS.svg?)](https://www.ardu-badge.com/ReefwingAHRS) ![version](https://img.shields.io/github/v/tag/Reefwing-Software/Reefwing-AHRS) ![license](https://img.shields.io/badge/license-MIT-green) ![release](https://img.shields.io/github/release-date/Reefwing-Software/Reefwing-AHRS?color="red") ![open source](https://badgen.net/badge/open/source/blue?icon=github)
22

33
# Reefwing AHRS
44

@@ -10,6 +10,8 @@ An Attitude and Heading Reference System (AHRS) takes information from the Inert
1010

1111
Version 2.2.0 added support for the Nano 33 BLE Sense Rev. 2. Version 2 of the Nano 33 BLE Sense, replaces the LSM9DS1 9 axis IMU with a combination of two IMUs, the BMI270, a 6 axis gyro & accelerometer and the BMM150, a 3 axis magnetometer. In order to support the new hardware, it makes sense to separate the sensor processing from the sensor fusion algorithms. This library version also added the Kalman Filter Fusion option.
1212

13+
Version 2.3.0 added support for an Extended Kalman Filter and fixed a number of bugs.
14+
1315
A complete description of this library is available in our Medium article: [Reefwing AHRS Arduino Library for Drones](https://reefwing.medium.com/reefwing-ahrs-arduino-library-for-drones-part-1-6d6457231764).
1416

1517
## Reefwing IMU Types
@@ -24,7 +26,7 @@ The libraries that use this definition header are:
2426
- [ReefwingMPU6x00](https://github.com/Reefwing-Software/MPU6x00)
2527
- [ReefwingMPU6050](https://github.com/Reefwing-Software/Reefwing-MPU6050)
2628

27-
The Structs Defined in the IMU Types Library are:
29+
The Structs defined in the IMU Types Library are:
2830

2931
- `EulerAngles`
3032
- `InertialMessage`
@@ -62,7 +64,7 @@ To use the Reefwing AHRS Library, you need to also install the Reefwing_imuTypes
6264

6365
- Complementary Filter
6466
- Madgwick Filter
65-
- Kalman Filter
67+
- Extended Kalman Filter
6668
- Mahony Filter
6769
- Classic Complementary Filter: Euler Angles are updated using trigonometry
6870
- NONE: Euler angles are calculated but no sensor fusion filter is used.
@@ -216,7 +218,7 @@ This sketch is configured to work with the `MADGWICK`, `MAHONY`, `CLASSIC`, `COM
216218
- **Madgwick Filter** - uses quaternions and combines a complementary filter approach with adaptive gain to fuse the sensor data.
217219
- **Mahony Filter** - uses quaternions, another riff on complementary fusion and includes an error correction step to compensate for gyroscope bias.
218220
- **Classic Complementary Filter** - Euler Angles are updated using trigonometry, apart from this it is similar to the Quaternion version.
219-
- **Kalman Filter** - can be used to minimise the impact of noise in sensors.
221+
- **Extended Kalman Filter** - can be used to minimise the impact of noise in sensors.
220222
- **NONE** - Euler angles are calculated but no sensor fusion is used. This is useful for demonstrating why you need sensor fusion!
221223

222224
The loop frequency will vary with the board and fusion algorithm. The `COMPLEMENTARY` filter is a bit slower than the `MADGWICK` and `MAHONY`. We found that we experienced significant gyroscopic drift with the classic complementary filter. The Madgwick and Mahony filters fixes this issue but take a bit longer to settle on an angle. Of the two, Mahony is a bit faster than Madgwick, but the best filter and associated free parameter settings will depend on the application. The `NONE` option is also fast but has crazy gyroscopic drift.

examples/betaOptimisation/betaOptimisation.ino

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@
1414
2.0.1 Invert Gyro Values PR 24/12/22
1515
2.1.0 Updated Fusion Library 30/12/22
1616
2.2.0 Add support for Nano 33 BLE Sense Rev. 2 10/02/23
17+
2.3.0 Fixed RMS Error calculation 22/11/24
1718
1819
This sketch attempts to determine the optimum beta for the
19-
Madgwick filter. The Nano 33 BLE should be placed flat and
20+
Madgwick filter. The Nano 33 BLE R1 should be placed flat and
2021
not moved for the duration of the test.
2122
2223
The default sample rate for this library is 238 Hz for the
@@ -44,17 +45,15 @@ float sampleArray[SAMPLES_PER_BETA];
4445
float err, optimumBeta, minError = INFINITY;
4546
int ctr = 0; // Track samples collected for each value of beta
4647

47-
float rms(float arr[]) {
48+
float rms(float arr[], int n) {
4849
float square = 0.0, mean = 0.0;
49-
int n = sizeof(arr) / sizeof(arr[0]);
50-
50+
5151
// Calculate square.
5252
for (int i = 0; i < n; i++) {
5353
square += pow(arr[i], 2);
5454
}
55-
56-
mean = (square / (float)(n)); // Calculate Mean.
57-
55+
56+
mean = square / (float)n; // Calculate Mean.
5857
return sqrt(mean);
5958
}
6059

@@ -63,10 +62,8 @@ void setup() {
6362
imu.begin();
6463
ahrs.begin();
6564

66-
// Positive magnetic declination - Sydney, AUSTRALIA
67-
ahrs.setDeclination(12.717);
6865
ahrs.setFusionAlgorithm(SensorFusion::MADGWICK);
69-
ahrs.setBeta(beta);
66+
ahrs.setDeclination(12.717); // Sydney, AUSTRALIA
7067

7168
// Start Serial and wait for connection
7269
Serial.begin(115200);
@@ -108,15 +105,15 @@ void setup() {
108105
ahrs.setData(imu.data);
109106
ahrs.update();
110107

111-
// load sampleArray with measured pitch and roll angles
108+
// load sampleArray with either pitch or roll angles
112109
sampleArray[ctr] = ahrs.angles.roll;
113110
ctr++;
114-
sampleArray[ctr] = ahrs.angles.pitch;
115-
ctr++;
111+
// sampleArray[ctr] = ahrs.angles.pitch;
112+
// ctr++;
116113
delay(10); // Wait for new sample
117114
}
118115
ctr = 0;
119-
err = rms(sampleArray);
116+
err = rms(sampleArray, SAMPLES_PER_BETA);
120117
if (err < minError) {
121118
minError = err;
122119
optimumBeta = beta;
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/******************************************************************
2+
@file betaOptimisationR2.ino
3+
@brief Determine optimal beta for Madgwick Filter
4+
@author David Such
5+
@copyright Please see the accompanying LICENSE file.
6+
7+
Code: David Such
8+
Version: 1.0.0
9+
Date: 22/11/24
10+
11+
1.0.0 Original Release. 22/11/24
12+
13+
This sketch targets the Nano 33 BLE Rev. 2 and attempts
14+
to determine the optimum beta for the Madgwick filter.
15+
The Nano 33 BLE should be placed flat and not moved for
16+
the duration of the test.
17+
18+
The default sample rate for this library is 100 Hz for the
19+
gyro/accelerometer and 10 Hz for the magnetometer. Thus a
20+
new gyro/acc reading should be available every 10 ms. We delay
21+
for 15 ms between readings.
22+
23+
******************************************************************/
24+
25+
#include <ReefwingAHRS.h>
26+
#include <Arduino_BMI270_BMM150.h>
27+
28+
ReefwingAHRS ahrs;
29+
SensorData data;
30+
31+
#define MIN_BETA 0.0
32+
#define MAX_BETA 0.5
33+
#define BETA_INC 0.01
34+
#define SAMPLES_PER_BETA 20
35+
#define GYRO_MEAS_ERROR M_PI * (40.0f / 180.0f) // gyroscope measurement error in rads/s (default is 40 deg/s)
36+
#define DEFAULT_BETA sqrt(3.0f / 4.0f) * GYRO_MEAS_ERROR
37+
38+
float beta = MIN_BETA; // Madgwick filter gain, Beta
39+
float sampleArray[SAMPLES_PER_BETA];
40+
float err, optimumBeta, minError = INFINITY;
41+
int ctr = 0; // Track samples collected for each value of beta
42+
43+
float rms(float arr[], int n) {
44+
float square = 0.0, mean = 0.0;
45+
46+
// Calculate square.
47+
for (int i = 0; i < n; i++) {
48+
square += pow(arr[i], 2);
49+
}
50+
51+
mean = square / (float)n; // Calculate Mean.
52+
return sqrt(mean);
53+
}
54+
55+
void setup() {
56+
// Initialise the AHRS
57+
ahrs.begin();
58+
59+
// Positive magnetic declination - Sydney, AUSTRALIA
60+
ahrs.setFusionAlgorithm(SensorFusion::MADGWICK);
61+
ahrs.setDeclination(12.717);
62+
63+
// Start Serial and wait for connection
64+
Serial.begin(115200);
65+
while (!Serial);
66+
67+
Serial.print("Detected Board - ");
68+
Serial.println(ahrs.getBoardTypeString());
69+
70+
if (IMU.begin()) {
71+
Serial.println("BMI270 & BMM150 IMUs Connected.");
72+
73+
delay(20);
74+
// Flush the first reading - this is important!
75+
// Particularly after changing the configuration.
76+
IMU.readGyroscope(data.gx, data.gy, data.gz);
77+
IMU.readAcceleration(data.ax, data.ay, data.az);
78+
79+
// Start processing IMU data.
80+
Serial.println("Starting Beta Optimisation.\n");
81+
Serial.println("Nano 33 BLE should remain motionless for duration of test.\n");
82+
delay(1000);
83+
Serial.print("Beta"); Serial.println("\tRMS Error");
84+
}
85+
else {
86+
Serial.println("IMU Not Detected.");
87+
while(1);
88+
}
89+
90+
while (beta < MAX_BETA) {
91+
while (ctr < SAMPLES_PER_BETA) {
92+
// Check for new IMU data and update angles
93+
if (IMU.gyroscopeAvailable()) { IMU.readGyroscope(data.gx, data.gy, data.gz); }
94+
if (IMU.accelerationAvailable()) { IMU.readAcceleration(data.ax, data.ay, data.az); }
95+
if (IMU.magneticFieldAvailable()) { IMU.readMagneticField(data.mx, data.my, data.mz); }
96+
97+
ahrs.setData(data);
98+
ahrs.update();
99+
100+
// load sampleArray with either pitch or roll angles
101+
sampleArray[ctr] = ahrs.angles.roll;
102+
ctr++;
103+
// sampleArray[ctr] = ahrs.angles.pitch;
104+
// ctr++;
105+
delay(15); // Wait for new sample
106+
}
107+
ctr = 0;
108+
err = rms(sampleArray, SAMPLES_PER_BETA);
109+
if (err < minError) {
110+
minError = err;
111+
optimumBeta = beta;
112+
}
113+
114+
Serial.print(beta); Serial.print("\t"); Serial.println(err);
115+
beta += BETA_INC;
116+
}
117+
118+
Serial.print("\nOptimum Beta = ");
119+
Serial.print(optimumBeta);
120+
Serial.print(", with an RMS error of ");
121+
Serial.print(minError);
122+
Serial.println(" degrees.");
123+
Serial.print("Default Beta = ");
124+
Serial.println(DEFAULT_BETA);
125+
Serial.println("\nTEST CONCLUDED");
126+
}
127+
128+
void loop() { }

examples/filterTest/filterTest.ino

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
/******************************************************************
2+
@file filter_test.ino
3+
@brief The sketch simulates IMU data with and without noise,
4+
evaluates singularities, and measures accuracy and drift.
5+
6+
@author David Such
7+
@copyright Please see the accompanying LICENSE file.
8+
9+
Code: David Such
10+
Version: 1.0.0
11+
Date: 23/11/24
12+
13+
1.0.0 Original Release. 23/11/24
14+
15+
******************************************************************/
16+
17+
#include <ReefwingAHRS.h>
18+
19+
// Select the simulation parameters
20+
#define SIMULATION_DURATION 20.0f // Seconds
21+
#define SIMULATION_STEP 0.01f // Seconds (100 Hz sampling rate)
22+
#define NOISE_STDDEV 0.02f // Standard deviation of noise for acceleration and gyro (rad/s and g)
23+
#define ACTUAL_YAW 30.0f // Ground truth yaw (degrees) - fixed
24+
#define MAX_ROLL 30.0f // Limit of synthesized roll angle ±MAX_ROLL (degrees)
25+
#define MAX_PITCH 20.0f // Limit of synthesized pitch angle ±MAX_PITCH (degrees)
26+
27+
// Instantiate AHRS
28+
ReefwingAHRS ahrs;
29+
SensorData simulatedData;
30+
31+
// Timing variables
32+
unsigned long startTime;
33+
float currentTime;
34+
35+
// Output variables
36+
float rollError, pitchError, yawError;
37+
38+
// Function to add Gaussian noise
39+
float addNoise(float value, float stddev) {
40+
return value + (stddev * ((float)random(-1000, 1000) / 1000.0f)); // Simulate noise
41+
}
42+
43+
// Function to simulate ideal IMU data
44+
void simulateIMUData(float t, bool addNoiseFlag, float &actualRoll, float &actualPitch, float &actualYaw) {
45+
// Ground truth: sinusoidal roll, pitch; constant yaw
46+
actualRoll = MAX_ROLL * sin(2 * PI * 0.1f * t); // Roll in degrees
47+
actualPitch = MAX_PITCH * cos(2 * PI * 0.1f * t); // Pitch in degrees
48+
actualYaw = ACTUAL_YAW; // Yaw in degrees
49+
50+
// Simulate gyro data (rad/s)
51+
simulatedData.gx = radians(2 * PI * 0.1f * MAX_ROLL * cos(2 * PI * 0.1f * t)); // d(roll)/dt
52+
simulatedData.gy = radians(-2 * PI * 0.1f * MAX_PITCH * sin(2 * PI * 0.1f * t)); // d(pitch)/dt
53+
simulatedData.gz = 0.0f; // d(yaw)/dt
54+
55+
// Simulate accelerometer data (g)
56+
simulatedData.ax = sin(radians(actualPitch));
57+
simulatedData.ay = -sin(radians(actualRoll)) * cos(radians(actualPitch));
58+
simulatedData.az = cos(radians(actualRoll)) * cos(radians(actualPitch));
59+
60+
// Simulate magnetometer data (arbitrary units)
61+
simulatedData.mx = cos(radians(actualYaw)) * cos(radians(actualPitch));
62+
simulatedData.my = sin(radians(actualYaw)) * cos(radians(actualPitch));
63+
simulatedData.mz = -sin(radians(actualPitch));
64+
65+
// Add noise if required
66+
if (addNoiseFlag) {
67+
simulatedData.gx = addNoise(simulatedData.gx, NOISE_STDDEV);
68+
simulatedData.gy = addNoise(simulatedData.gy, NOISE_STDDEV);
69+
simulatedData.gz = addNoise(simulatedData.gz, NOISE_STDDEV);
70+
71+
simulatedData.ax = addNoise(simulatedData.ax, NOISE_STDDEV);
72+
simulatedData.ay = addNoise(simulatedData.ay, NOISE_STDDEV);
73+
simulatedData.az = addNoise(simulatedData.az, NOISE_STDDEV);
74+
75+
simulatedData.mx = addNoise(simulatedData.mx, NOISE_STDDEV);
76+
simulatedData.my = addNoise(simulatedData.my, NOISE_STDDEV);
77+
simulatedData.mz = addNoise(simulatedData.mz, NOISE_STDDEV);
78+
}
79+
80+
// Set the simulated data in the AHRS library
81+
ahrs.setData(simulatedData);
82+
}
83+
84+
// Test a single filter
85+
void testFilter(SensorFusion filter, bool addNoiseFlag) {
86+
ahrs.reset();
87+
ahrs.setFusionAlgorithm(filter);
88+
ahrs.setDOF(DOF::DOF_9);
89+
90+
Serial.print("Testing filter: ");
91+
Serial.print((int)filter);
92+
Serial.println(addNoiseFlag ? " with noise" : " without noise");
93+
Serial.println("Time,Roll Error,Pitch Error,Yaw Error");
94+
95+
currentTime = 0.0f;
96+
startTime = millis();
97+
98+
while (currentTime < SIMULATION_DURATION) {
99+
float actualRoll, actualPitch, actualYaw;
100+
101+
simulateIMUData(currentTime, addNoiseFlag, actualRoll, actualPitch, actualYaw);
102+
ahrs.update();
103+
104+
// Calculate errors using ground-truth angles
105+
rollError = ahrs.angles.roll - actualRoll; // Error in roll angle
106+
pitchError = ahrs.angles.pitch - actualPitch; // Error in pitch angle
107+
yawError = ahrs.angles.yaw - actualYaw; // Error in yaw angle
108+
109+
// Output results
110+
Serial.print(currentTime, 3);
111+
Serial.print(",");
112+
Serial.print(rollError, 3);
113+
Serial.print(",");
114+
Serial.print(pitchError, 3);
115+
Serial.print(",");
116+
Serial.println(yawError, 3);
117+
118+
currentTime += SIMULATION_STEP;
119+
delay(SIMULATION_STEP * 1000);
120+
}
121+
}
122+
123+
void setup() {
124+
// Start Serial and wait for connection
125+
Serial.begin(115200);
126+
while (!Serial);
127+
128+
// Initialize AHRS
129+
ahrs.begin();
130+
131+
// Test each filter
132+
for (int i = 0; i <= (int)SensorFusion::NONE; i++) {
133+
testFilter(static_cast<SensorFusion>(i), false); // Test without noise
134+
testFilter(static_cast<SensorFusion>(i), true); // Test with noise
135+
}
136+
}
137+
138+
void loop() { }

0 commit comments

Comments
 (0)