Skip to content

Commit 6443bef

Browse files
committed
fleshing out examples and adding filters for computing pitch/roll from 6-axis imu
1 parent 9296cda commit 6443bef

14 files changed

+878
-369
lines changed

.github/workflows/build.yml

+2
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ jobs:
8181
target: esp32s3
8282
- path: 'components/i2c/example'
8383
target: esp32
84+
- path: 'components/icm42607/example'
85+
target: esp32s3
8486
- path: 'components/interrupt/example'
8587
target: esp32s3
8688
- path: 'components/joystick/example'

components/esp-box/example/CMakeLists.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ set(EXTRA_COMPONENT_DIRS
1111

1212
set(
1313
COMPONENTS
14-
"main esptool_py esp-box"
14+
"main esptool_py esp-box filters"
1515
CACHE STRING
1616
"List of components to include"
1717
)

components/esp-box/example/main/esp_box_example.cpp

+63-11
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
#include "esp-box.hpp"
77

8+
#include "kalman_filter.hpp"
9+
810
using namespace std::chrono_literals;
911

1012
static constexpr size_t MAX_CIRCLES = 100;
@@ -93,9 +95,24 @@ extern "C" void app_main(void) {
9395

9496
// add text in the center of the screen
9597
lv_obj_t *label = lv_label_create(lv_screen_active());
96-
lv_label_set_text(label, "Touch the screen!\nPress the home button to clear circles.");
97-
lv_obj_align(label, LV_ALIGN_CENTER, 0, 0);
98-
lv_obj_set_style_text_align(label, LV_TEXT_ALIGN_CENTER, 0);
98+
static std::string label_text =
99+
"\n\n\n\n\nTouch the screen!\nPress the home button to clear circles.";
100+
lv_label_set_text(label, label_text.c_str());
101+
lv_obj_align(label, LV_ALIGN_TOP_LEFT, 0, 0);
102+
lv_obj_set_style_text_align(label, LV_TEXT_ALIGN_LEFT, 0);
103+
104+
/*Create style*/
105+
static lv_style_t style_line;
106+
lv_style_init(&style_line);
107+
lv_style_set_line_width(&style_line, 8);
108+
lv_style_set_line_color(&style_line, lv_palette_main(LV_PALETTE_BLUE));
109+
lv_style_set_line_rounded(&style_line, true);
110+
111+
// make a line for showing the direction of "down"
112+
lv_obj_t *line = lv_line_create(lv_screen_active());
113+
static lv_point_precise_t line_points[] = {{0, 0}, {box.lcd_width(), box.lcd_height()}};
114+
lv_line_set_points(line, line_points, 2);
115+
lv_obj_add_style(line, &style_line, 0);
99116

100117
// add a button in the top left which (when pressed) will rotate the display
101118
// through 0, 90, 180, 270 degrees
@@ -135,6 +152,7 @@ extern "C" void app_main(void) {
135152
},
136153
.task_config = {
137154
.name = "lv_task",
155+
.stack_size_bytes = 6 * 1024,
138156
}});
139157
lv_task.start();
140158

@@ -151,19 +169,19 @@ extern "C" void app_main(void) {
151169

152170
// make a task to read out the IMU data and print it to console
153171
espp::Task imu_task(
154-
{.callback = [&label](std::mutex &m, std::condition_variable &cv) -> bool {
172+
{.callback = [&label, &line](std::mutex &m, std::condition_variable &cv) -> bool {
155173
// sleep first in case we don't get IMU data and need to exit early
156174
{
157175
std::unique_lock<std::mutex> lock(m);
158-
cv.wait_for(lock, 100ms);
176+
cv.wait_for(lock, 10ms);
159177
}
160178
static auto &box = espp::EspBox::get();
161179
static auto imu = box.imu();
162180

163-
auto now = std::chrono::steady_clock::now();
181+
auto now = esp_timer_get_time(); // time in microseconds
164182
static auto t0 = now;
165183
auto t1 = now;
166-
float dt = std::chrono::duration<float>(t1 - t0).count();
184+
float dt = (t1 - t0) / 1'000'000.0f; // convert us to s
167185
t0 = t1;
168186

169187
std::error_code ec;
@@ -181,17 +199,51 @@ extern "C" void app_main(void) {
181199
return false;
182200
}
183201

184-
static espp::icm42607::ComplimentaryAngle angle{};
185-
angle = imu->complimentary_filter(dt, angle, accel, gyro);
202+
float roll = 0, pitch = 0;
203+
204+
// with only the accelerometer + gyroscope, we can't get yaw :(
205+
static espp::KalmanFilter kalmanPitch;
206+
static espp::KalmanFilter kalmanRoll;
207+
208+
// Compute pitch and roll from accelerometer
209+
float accelPitch =
210+
atan2(accel.y, sqrt(accel.x * accel.x + accel.z * accel.z)) * 180.0f / M_PI;
211+
float accelRoll = atan2(-accel.x, accel.z) * 180.0f / M_PI;
186212

187-
std::string text = "Touch the screen!\nPress the home button to clear circles.\n";
213+
// Apply Kalman filter
214+
pitch = kalmanPitch.update(accelPitch, gyro.y, dt);
215+
roll = kalmanRoll.update(accelRoll, gyro.x, dt);
216+
217+
std::string text = fmt::format("{}\n\n\n\n", label_text);
188218
text += fmt::format("Accel: {:02.2f} {:02.2f} {:02.2f}\n", accel.x, accel.y, accel.z);
189219
text += fmt::format("Gyro: {:03.2f} {:03.2f} {:03.2f}\n", gyro.x, gyro.y, gyro.z);
190-
text += fmt::format("Angle: {:03.2f} {:03.2f}\n", angle.roll, angle.pitch);
220+
text += fmt::format("Angle: {:03.2f} {:03.2f}\n", roll, pitch);
191221
text += fmt::format("Temp: {:02.1f} C\n", temp);
192222

223+
float rollRad = roll * M_PI / 180.0f;
224+
float pitchRad = pitch * M_PI / 180.0f;
225+
226+
// use the pitch to to draw a line on the screen indiating the
227+
// direction from the center of the screen to "down"
228+
int x0 = box.lcd_width() / 2;
229+
int y0 = box.lcd_height() / 2;
230+
int x1 = x0 - 50 * cos(-pitchRad);
231+
int y1 = y0 + 50 * sin(-pitchRad);
232+
233+
float vx = sin(pitchRad);
234+
float vy = -cos(pitchRad) * sin(rollRad);
235+
float vz = -cos(pitchRad) * cos(rollRad);
236+
237+
x1 = x0 - 50 * vy;
238+
y1 = y0 - 50 * vx;
239+
240+
static lv_point_precise_t line_points[] = {{x0, y0}, {x1, y1}};
241+
line_points[1].x = x1;
242+
line_points[1].y = y1;
243+
193244
std::lock_guard<std::recursive_mutex> lock(lvgl_mutex);
194245
lv_label_set_text(label, text.c_str());
246+
lv_line_set_points(line, line_points, 2);
195247

196248
return false;
197249
},

components/filters/CMakeLists.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
idf_component_register(
22
INCLUDE_DIRS "include"
33
SRC_DIRS "src"
4-
REQUIRES esp_timer format esp-dsp)
4+
REQUIRES esp_timer format esp-dsp math)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
#pragma once
2+
3+
#include <cmath>
4+
5+
#include "fast_math.hpp"
6+
7+
namespace espp {
8+
/// Complementary filter for estimating pitch and roll angles from
9+
/// accelerometer and gyroscope readings.
10+
/// The filter is defined by the following equation:
11+
/// angle = alpha * (angle + gyro * dt) + (1 - alpha) * accel
12+
/// where:
13+
/// - angle is the estimated angle,
14+
/// - alpha is the filter coefficient (0 < alpha < 1),
15+
/// - gyro is the gyroscope reading,
16+
/// - accel is the accelerometer reading,
17+
/// - dt is the time step.
18+
class ComplementaryFilter {
19+
public:
20+
/// Constructor
21+
/// \param alpha Filter coefficient (0 < alpha < 1)
22+
ComplementaryFilter(float alpha = 0.98)
23+
: alpha(alpha)
24+
, pitch(0.0f)
25+
, roll(0.0f) {}
26+
27+
/// Update the filter with accelerometer and gyroscope readings.
28+
/// \param accel Accelerometer readings (m/s^2)
29+
/// \param gyro Gyroscope readings (degrees/s)
30+
/// \param dt Time step (s)
31+
/// \note The accelerometer and gyroscope readings must be in the
32+
/// same coordinate system.
33+
void update(const std::span<const float, 3> &accel, const std::span<const float, 3> &gyro,
34+
float dt) {
35+
update(accel[0], accel[1], accel[2], gyro[0], gyro[1], gyro[2], dt);
36+
}
37+
38+
/// Update the filter with accelerometer and gyroscope readings.
39+
/// \param ax Accelerometer x-axis reading (m/s^2)
40+
/// \param ay Accelerometer y-axis reading (m/s^2)
41+
/// \param az Accelerometer z-axis reading (m/s^2)
42+
/// \param gx Gyroscope x-axis reading (degrees/s)
43+
/// \param gy Gyroscope y-axis reading (degrees/s)
44+
/// \param gz Gyroscope z-axis reading (degrees/s)
45+
/// \param dt Time step (s)
46+
/// \note The accelerometer and gyroscope readings must be in the
47+
/// same coordinate system.
48+
void update(float ax, float ay, float az, float gx, float gy, float gz, float dt) {
49+
// Convert gyroscope readings from degrees/s to radians/s
50+
gx = gx * M_PI / 180.0f;
51+
gy = gy * M_PI / 180.0f;
52+
53+
// Compute pitch and roll from accelerometer (degrees)
54+
float accelPitch = atan2(ay, sqrt(ax * ax + az * az)) * 180.0f / M_PI;
55+
float accelRoll = atan2(-ax, az) * 180.0f / M_PI;
56+
57+
// Integrate gyroscope readings
58+
float gyroPitch = pitch + gy * dt;
59+
float gyroRoll = roll + gx * dt;
60+
61+
// Apply complementary filter
62+
pitch = alpha * gyroPitch + (1 - alpha) * accelPitch;
63+
roll = alpha * gyroRoll + (1 - alpha) * accelRoll;
64+
}
65+
66+
/// Get the estimated pitch angle (degrees)
67+
/// \return The estimated pitch angle
68+
float get_pitch() const { return pitch; }
69+
70+
/// Get the estimated roll angle (degrees)
71+
/// \return The estimated roll angle
72+
float get_roll() const { return roll; }
73+
74+
protected:
75+
float alpha;
76+
float pitch, roll;
77+
}; // class ComplimentaryFilter
78+
} // namespace espp
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
#pragma once
2+
3+
#include <cmath>
4+
5+
#include "fast_math.hpp"
6+
7+
namespace espp {
8+
/// Kalman Filter for angle estimation
9+
///
10+
/// This class implements a simple Kalman filter for estimating the angle of a
11+
/// system based on accelerometer and gyroscope measurements. The filter is
12+
/// based on the following state-space model:
13+
/// x(k+1) = A * x(k) + B * u(k) + w(k)
14+
/// z(k) = H * x(k) + v(k)
15+
/// where:
16+
/// - x(k) is the state vector at time k
17+
/// - u(k) is the control input at time k
18+
/// - z(k) is the measurement at time k
19+
/// - w(k) is the process noise at time k
20+
/// - v(k) is the measurement noise at time k
21+
/// - A, B, and H are matrices
22+
/// - A is the state transition matrix
23+
/// - B is the control input matrix
24+
/// - H is the measurement matrix
25+
/// The Kalman filter estimates the state x(k) based on the measurements z(k).
26+
/// The filter is implemented in two steps: prediction and correction.
27+
/// The prediction step estimates the state at the next time step based on the
28+
/// current state and the control input. The correction step updates the state
29+
/// estimate based on the measurement.
30+
class KalmanFilter {
31+
public:
32+
/// Constructor
33+
KalmanFilter() {
34+
angle = 0.0f;
35+
bias = 0.0f;
36+
P[0][0] = 1.0f;
37+
P[0][1] = 0.0f;
38+
P[1][0] = 0.0f;
39+
P[1][1] = 1.0f;
40+
41+
Q_angle = 0.001f; // Process noise variance for accelerometer
42+
Q_bias = 0.003f; // Process noise variance for gyro bias
43+
R_measure = 0.03f; // Measurement noise variance
44+
}
45+
46+
/// Update the filter with a new measurement
47+
/// \param newAngle The new angle measurement
48+
/// \param newRate The new rate measurement
49+
/// \param dt The time step
50+
/// \return The updated angle estimate
51+
float update(float newAngle, float newRate, float dt) {
52+
// Prediction step
53+
rate = newRate - bias;
54+
angle += dt * rate;
55+
56+
P[0][0] += dt * (dt * P[1][1] - P[0][1] - P[1][0] + Q_angle);
57+
P[0][1] -= dt * P[1][1];
58+
P[1][0] -= dt * P[1][1];
59+
P[1][1] += Q_bias * dt;
60+
61+
// Correction step
62+
float S = P[0][0] + R_measure;
63+
float K[2] = {P[0][0] / S, P[1][0] / S};
64+
65+
float y = newAngle - angle;
66+
angle += K[0] * y;
67+
bias += K[1] * y;
68+
69+
// Update error covariance matrix
70+
float P00_temp = P[0][0], P01_temp = P[0][1];
71+
P[0][0] -= K[0] * P00_temp;
72+
P[0][1] -= K[0] * P01_temp;
73+
P[1][0] -= K[1] * P00_temp;
74+
P[1][1] -= K[1] * P01_temp;
75+
76+
return angle;
77+
}
78+
79+
protected:
80+
float angle, bias, rate;
81+
float P[2][2]; // Error covariance matrix
82+
float Q_angle, Q_bias, R_measure;
83+
}; // class KalmanFilter
84+
} // namespace espp
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
#pragma once
2+
3+
#include <cmath>
4+
5+
#include "fast_math.hpp"
6+
7+
namespace espp {
8+
class MadgwickFilter {
9+
public:
10+
MadgwickFilter(float beta = 0.1f, float sampleFreq = 100.0f)
11+
: beta(beta)
12+
, sampleFreq(sampleFreq)
13+
, q0(1.0f)
14+
, q1(0.0f)
15+
, q2(0.0f)
16+
, q3(0.0f) {}
17+
18+
void update(float gx, float gy, float gz, float ax, float ay, float az) {
19+
float recipNorm;
20+
float s0, s1, s2, s3;
21+
float qDot1, qDot2, qDot3, qDot4;
22+
23+
// Convert gyroscope from degrees/s to radians/s
24+
gx *= M_PI / 180.0f;
25+
gy *= M_PI / 180.0f;
26+
gz *= M_PI / 180.0f;
27+
28+
// Compute quaternion derivatives
29+
qDot1 = 0.5f * (-q1 * gx - q2 * gy - q3 * gz);
30+
qDot2 = 0.5f * (q0 * gx + q2 * gz - q3 * gy);
31+
qDot3 = 0.5f * (q0 * gy - q1 * gz + q3 * gx);
32+
qDot4 = 0.5f * (q0 * gz + q1 * gy - q2 * gx);
33+
34+
// Normalize accelerometer measurement
35+
recipNorm = 1.0f / sqrt(ax * ax + ay * ay + az * az);
36+
ax *= recipNorm;
37+
ay *= recipNorm;
38+
az *= recipNorm;
39+
40+
// Compute feedback correction terms
41+
s0 = 2.0f * (q2 * q3 - q0 * q1) - ax;
42+
s1 = 2.0f * (q0 * q2 + q1 * q3) - ay;
43+
s2 = 1.0f - 2.0f * (q1 * q1 + q2 * q2) - az;
44+
s3 = 2.0f * (q1 * q2 + q0 * q3) - az; // Corrected: Missing s3 term
45+
46+
// Normalize correction
47+
recipNorm = 1.0f / sqrt(s0 * s0 + s1 * s1 + s2 * s2 + s3 * s3);
48+
s0 *= recipNorm;
49+
s1 *= recipNorm;
50+
s2 *= recipNorm;
51+
s3 *= recipNorm;
52+
53+
// Compute feedback
54+
qDot1 -= beta * s0;
55+
qDot2 -= beta * s1;
56+
qDot3 -= beta * s2;
57+
qDot4 -= beta * s3; // Corrected: s3 was missing before
58+
59+
// Integrate quaternion
60+
q0 += qDot1 * (1.0f / sampleFreq);
61+
q1 += qDot2 * (1.0f / sampleFreq);
62+
q2 += qDot3 * (1.0f / sampleFreq);
63+
q3 += qDot4 * (1.0f / sampleFreq);
64+
65+
// Normalize quaternion
66+
recipNorm = 1.0f / sqrt(q0 * q0 + q1 * q1 + q2 * q2 + q3 * q3);
67+
q0 *= recipNorm;
68+
q1 *= recipNorm;
69+
q2 *= recipNorm;
70+
q3 *= recipNorm;
71+
}
72+
73+
void get_euler(float &pitch, float &roll, float &yaw) {
74+
pitch = atan2(2.0f * (q0 * q1 + q2 * q3), 1.0f - 2.0f * (q1 * q1 + q2 * q2)) * 180.0f / M_PI;
75+
roll = asin(2.0f * (q0 * q2 - q3 * q1)) * 180.0f / M_PI;
76+
yaw = atan2(2.0f * (q0 * q3 + q1 * q2), 1.0f - 2.0f * (q2 * q2 + q3 * q3)) * 180.0f / M_PI;
77+
}
78+
79+
protected:
80+
float beta, sampleFreq;
81+
float q0, q1, q2, q3; // Quaternion values
82+
};
83+
} // namespace espp

0 commit comments

Comments
 (0)