2 * This file is part of Cleanflight.
4 * Cleanflight is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
9 * Cleanflight is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with Cleanflight. If not, see <http://www.gnu.org/licenses/>.
25 #include "build/debug.h"
27 #include "common/axis.h"
28 #include "common/maths.h"
29 #include "common/vector.h"
31 #include "config/feature.h"
33 #include "pg/pg_ids.h"
36 #include "drivers/accgyro/accgyro.h"
37 #include "drivers/compass/compass.h"
38 #include "drivers/sensor.h"
40 #include "fc/rc_controls.h"
41 #include "fc/rc_modes.h"
42 #include "fc/runtime_config.h"
45 #include "flight/imu.h"
46 #include "flight/mixer.h"
47 #include "flight/pid.h"
48 #include "flight/position.h"
54 #include "pg/autopilot.h"
56 #include "sensors/acceleration.h"
57 #include "sensors/barometer.h"
58 #include "sensors/compass.h"
59 #include "sensors/gyro.h"
60 #include "sensors/sensors.h"
62 void imuComputeRotationMatrix(void);
63 void imuUpdateEulerAngles(void);
64 void imuMahonyAHRSupdate(float dt
,
65 float gx
, float gy
, float gz
,
66 bool useAcc
, float ax
, float ay
, float az
,
67 float headingErrMag
, float headingErrCog
,
68 const float dcmKpGain
);
69 float imuCalcMagErr(void);
70 float imuCalcCourseErr(float courseOverGround
);
71 extern quaternion_t q
;
72 extern matrix33_t rMat
;
73 extern bool attitudeIsEstablished
;
75 PG_REGISTER(rcControlsConfig_t
, rcControlsConfig
, PG_RC_CONTROLS_CONFIG
, 0);
76 PG_REGISTER(barometerConfig_t
, barometerConfig
, PG_BAROMETER_CONFIG
, 0);
77 PG_REGISTER(gpsConfig_t
, gpsConfig
, PG_GPS_CONFIG
, 0);
78 PG_REGISTER(apConfig_t
, apConfig
, PG_AUTOPILOT
, 0);
80 PG_RESET_TEMPLATE(featureConfig_t
, featureConfig
,
85 #include "unittest_macros.h"
86 #include "gtest/gtest.h"
88 const float sqrt2over2
= sqrtf(2) / 2.0f
;
90 void quaternion_from_axis_angle(quaternion_t
* q
, float angle
, float x
, float y
, float z
) {
91 vector3_t a
= {{x
, y
, z
}};
92 vector3Normalize(&a
, &a
);
93 q
->w
= cos(angle
/ 2);
94 q
->x
= a
.x
* sin(angle
/ 2);
95 q
->y
= a
.y
* sin(angle
/ 2);
96 q
->z
= a
.z
* sin(angle
/ 2);
99 TEST(FlightImuTest
, TestCalculateRotationMatrix
)
109 imuComputeRotationMatrix();
111 EXPECT_FLOAT_EQ(1.0f
, rMat
.m
[0][0]);
112 EXPECT_FLOAT_EQ(0.0f
, rMat
.m
[0][1]);
113 EXPECT_FLOAT_EQ(0.0f
, rMat
.m
[0][2]);
114 EXPECT_FLOAT_EQ(0.0f
, rMat
.m
[1][0]);
115 EXPECT_FLOAT_EQ(1.0f
, rMat
.m
[1][1]);
116 EXPECT_FLOAT_EQ(0.0f
, rMat
.m
[1][2]);
117 EXPECT_FLOAT_EQ(0.0f
, rMat
.m
[2][0]);
118 EXPECT_FLOAT_EQ(0.0f
, rMat
.m
[2][1]);
119 EXPECT_FLOAT_EQ(1.0f
, rMat
.m
[2][2]);
121 // 90 degrees around Z axis
127 imuComputeRotationMatrix();
129 EXPECT_NEAR(0.0f
, rMat
.m
[0][0], TOL
);
130 EXPECT_NEAR(-1.0f
, rMat
.m
[0][1], TOL
);
131 EXPECT_NEAR(0.0f
, rMat
.m
[0][2], TOL
);
132 EXPECT_NEAR(1.0f
, rMat
.m
[1][0], TOL
);
133 EXPECT_NEAR(0.0f
, rMat
.m
[1][1], TOL
);
134 EXPECT_NEAR(0.0f
, rMat
.m
[1][2], TOL
);
135 EXPECT_NEAR(0.0f
, rMat
.m
[2][0], TOL
);
136 EXPECT_NEAR(0.0f
, rMat
.m
[2][1], TOL
);
137 EXPECT_NEAR(1.0f
, rMat
.m
[2][2], TOL
);
139 // 60 degrees around X axis
145 imuComputeRotationMatrix();
147 EXPECT_NEAR(1.0f
, rMat
.m
[0][0], TOL
);
148 EXPECT_NEAR(0.0f
, rMat
.m
[0][1], TOL
);
149 EXPECT_NEAR(0.0f
, rMat
.m
[0][2], TOL
);
150 EXPECT_NEAR(0.0f
, rMat
.m
[1][0], TOL
);
151 EXPECT_NEAR(0.5f
, rMat
.m
[1][1], TOL
);
152 EXPECT_NEAR(-0.866f
, rMat
.m
[1][2], TOL
);
153 EXPECT_NEAR(0.0f
, rMat
.m
[2][0], TOL
);
154 EXPECT_NEAR(0.866f
, rMat
.m
[2][1], TOL
);
155 EXPECT_NEAR(0.5f
, rMat
.m
[2][2], TOL
);
158 TEST(FlightImuTest
, TestUpdateEulerAngles
)
161 memset(&rMat
, 0.0, sizeof(float) * 9);
163 imuUpdateEulerAngles();
165 EXPECT_EQ(0, attitude
.values
.roll
);
166 EXPECT_EQ(0, attitude
.values
.pitch
);
167 EXPECT_EQ(0, attitude
.values
.yaw
);
170 memset(&rMat
, 0.0, sizeof(float) * 9);
171 rMat
.m
[0][0] = sqrt2over2
;
172 rMat
.m
[0][1] = sqrt2over2
;
173 rMat
.m
[1][0] = -sqrt2over2
;
174 rMat
.m
[1][1] = sqrt2over2
;
176 imuUpdateEulerAngles();
178 EXPECT_EQ(0, attitude
.values
.roll
);
179 EXPECT_EQ(0, attitude
.values
.pitch
);
180 EXPECT_EQ(450, attitude
.values
.yaw
);
183 TEST(FlightImuTest
, TestSmallAngle
)
185 const float r1
= 0.898;
186 const float r2
= 0.438;
189 imuConfigMutable()->small_angle
= 25;
191 attitudeIsEstablished
= true;
194 memset(&rMat
, 0.0, sizeof(float) * 9);
197 imuComputeRotationMatrix();
200 EXPECT_FALSE(isUpright());
209 imuComputeRotationMatrix();
212 EXPECT_FALSE(isUpright());
215 memset(&rMat
, 0.0, sizeof(float) * 9);
218 imuComputeRotationMatrix();
221 EXPECT_FALSE(isUpright());
224 testing::AssertionResult
DoubleNearWrapPredFormat(const char* expr1
, const char* expr2
,
225 const char* abs_error_expr
, const char* wrap_expr
, double val1
,
226 double val2
, double abs_error
, double wrap
) {
227 const double diff
= remainder(val1
- val2
, wrap
);
228 if (fabs(diff
) <= abs_error
) return testing::AssertionSuccess();
230 return testing::AssertionFailure()
231 << "The difference between " << expr1
<< " and " << expr2
<< " is "
232 << diff
<< " (wrapped to 0 .. " << wrap_expr
<< ")"
233 << ", which exceeds " << abs_error_expr
<< ", where\n"
234 << expr1
<< " evaluates to " << val1
<< ",\n"
235 << expr2
<< " evaluates to " << val2
<< ", and\n"
236 << abs_error_expr
<< " evaluates to " << abs_error
<< ".";
239 #define EXPECT_NEAR_DEG(val1, val2, abs_error) \
240 EXPECT_PRED_FORMAT4(DoubleNearWrapPredFormat, val1, val2, \
243 #define EXPECT_NEAR_RAD(val1, val2, abs_error) \
244 EXPECT_PRED_FORMAT4(DoubleNearWrapPredFormat, val1, val2, \
249 class MahonyFixture
: public ::testing::Test
{
260 void SetUp() override
{
264 cogGain
= 0.0; // no cog
266 dcmKp
= .25; // default dcm_kp
267 dt
= 1e-2; // 100Hz update
270 // level, poiting north
271 setOrientationAA(0, {{1,0,0}}); // identity
273 virtual void setOrientationAA(float angleDeg
, vector3_t axis
) {
274 quaternion_from_axis_angle(&q
, DEGREES_TO_RADIANS(angleDeg
), axis
.x
, axis
.y
, axis
.z
);
275 imuComputeRotationMatrix();
278 float wrap(float angle
) {
279 angle
= fmod(angle
, 360);
280 if (angle
< 0) angle
+= 360;
283 float angleDiffNorm(vector3_t
*a
, vector3_t
* b
, vector3_t weight
= {{1,1,1}}) {
285 vector3Scale(&tmp
, b
, -1);
286 vector3Add(&tmp
, &tmp
, a
);
287 for (int i
= 0; i
< 3; i
++)
288 tmp
.v
[i
] *= weight
.v
[i
];
289 for (int i
= 0; i
< 3; i
++)
290 tmp
.v
[i
] = std::remainder(tmp
.v
[i
], 360.0);
291 return vector3Norm(&tmp
);
293 // run Mahony for some time
294 // return time it took to get within 1deg from target
295 float imuIntegrate(float runTime
, vector3_t
* target
) {
296 float alignTime
= -1;
297 for (float t
= 0; t
< runTime
; t
+= dt
) {
298 // if (fmod(t, 1) < dt) printf("MagBF=%.2f %.2f %.2f\n", magBF.x, magBF.y, magBF.z);
299 float headingErrMag
= 0;
300 if (useMag
) { // not implemented yet
301 headingErrMag
= imuCalcMagErr();
303 float headingErrCog
= 0;
305 headingErrCog
= imuCalcCourseErr(DEGREES_TO_RADIANS(cogDeg
)) * cogGain
;
308 imuMahonyAHRSupdate(dt
,
309 gyro
.x
, gyro
.y
, gyro
.z
,
310 useAcc
, acc
.x
, acc
.y
, acc
.z
,
311 headingErrMag
, headingErrCog
,
313 imuUpdateEulerAngles();
314 // if (fmod(t, 1) < dt) printf("%3.1fs - %3.1f %3.1f %3.1f\n", t, attitude.values.roll / 10.0f, attitude.values.pitch / 10.0f, attitude.values.yaw / 10.0f);
315 // remember how long it took
317 vector3_t rpy
= {{attitude
.values
.roll
/ 10.0f
, attitude
.values
.pitch
/ 10.0f
, attitude
.values
.yaw
/ 10.0f
}};
318 float error
= angleDiffNorm(&rpy
, target
);
327 class YawTest
: public MahonyFixture
, public testing::WithParamInterface
<float> {
330 TEST_P(YawTest
, TestCogAlign
)
334 const float rollDeg
= 30; // 30deg pitch forward
335 setOrientationAA(rollDeg
, {{0, 1, 0}});
336 vector3_t expect
= {{0, rollDeg
, wrap(cogDeg
)}};
337 // integrate IMU. about 25s is enough in worst case
338 float alignTime
= imuIntegrate(80, &expect
);
340 imuUpdateEulerAngles();
342 EXPECT_NEAR_DEG(attitude
.values
.roll
/ 10.0, expect
.x
, .1);
343 EXPECT_NEAR_DEG(attitude
.values
.pitch
/ 10.0, expect
.y
, .1);
344 // yaw is close to CoG direction
345 EXPECT_NEAR_DEG(attitude
.values
.yaw
/ 10.0, expect
.z
, 1); // error < 1 deg
346 if (alignTime
>= 0) {
347 printf("[ ] Aligned to %.f deg in %.2fs\n", cogDeg
, alignTime
);
351 TEST_P(YawTest
, TestMagAlign
)
353 float initialAngle
= GetParam();
355 // level, rotate to param heading
356 quaternion_from_axis_angle(&q
, -DEGREES_TO_RADIANS(initialAngle
), 0, 0, 1);
357 imuComputeRotationMatrix();
359 vector3_t expect
= {{0, 0, 0}}; // expect zero yaw
361 vector3_t magBF
= {{1, 0, .5}}; // use arbitrary Z component, point north
366 // integrate IMU. about 25s is enough in worst case
367 float alignTime
= imuIntegrate(30, &expect
);
369 imuUpdateEulerAngles();
371 EXPECT_NEAR_DEG(attitude
.values
.roll
/ 10.0, expect
.x
, .1);
372 EXPECT_NEAR_DEG(attitude
.values
.pitch
/ 10.0, expect
.y
, .1);
373 // yaw is close to north (0 deg)
374 EXPECT_NEAR_DEG(attitude
.values
.yaw
/ 10.0, expect
.z
, 1.0); // error < 1 deg
375 if (alignTime
>= 0) {
376 printf("[ ] Aligned from %.f deg in %.2fs\n", initialAngle
, alignTime
);
380 INSTANTIATE_TEST_SUITE_P(
383 0, 45, -45, 90, 180, 270, 720+45
389 extern boxBitmask_t rcModeActivationMask
;
391 float rcData
[MAX_SUPPORTED_RC_CHANNEL_COUNT
];
397 gpsSolutionData_t gpsSol
;
400 int16_t debug
[DEBUG16_VALUE_COUNT
];
403 uint16_t flightModeFlags
;
406 pidProfile_t
*currentPidProfile
;
408 uint16_t enableFlightMode(flightModeFlags_e mask
) {
409 return flightModeFlags
|= (mask
);
412 uint16_t disableFlightMode(flightModeFlags_e mask
) {
413 return flightModeFlags
&= ~(mask
);
416 bool sensors(uint32_t mask
) {
417 return mask
& SENSOR_ACC
;
420 uint32_t millis(void) { return 0; }
421 uint32_t micros(void) { return 0; }
423 bool compassIsHealthy(void) { return true; }
424 bool baroIsCalibrated(void) { return true; }
425 void performBaroCalibrationCycle(void) {}
426 float baroCalculateAltitude(void) { return 0; }
427 bool gyroGetAccumulationAverage(float *) { return false; }
428 bool accGetAccumulationAverage(float *) { return false; }
429 void mixerSetThrottleAngleCorrection(int) {};
430 bool gpsRescueIsRunning(void) { return false; }
431 bool isFixedWing(void) { return false; }
432 void pinioBoxTaskControl(void) {}
433 void schedulerIgnoreTaskExecTime(void) {}
434 void schedulerIgnoreTaskStateTime(void) {}
435 void schedulerSetNextStateTime(timeDelta_t
) {}
436 bool schedulerGetIgnoreTaskExecTime() { return false; }
437 float gyroGetFilteredDownsampled(int) { return 0.0f
; }
438 float baroUpsampleAltitude() { return 0.0f
; }
439 float getBaroAltitude(void) { return 3000.0f
; }
440 float gpsRescueGetImuYawCogGain(void) { return 1.0f
; }
441 float getRcDeflectionAbs(int) { return 0.0f
; }