Skip to content

Commit 04e62fe

Browse files
authored
feat(icm42670): Add ICM42607 / ICM42670 IMU component (#392)
* feat(icm42670): Add ICM42607 / ICM42670 IMU component * Add new component for IMU * Add IMU to esp-box component and test it in the example IMUs are nice, and exposing the IMU via the hardware component / bsp (esp-box in this case) is even better. Testing / showcasing the IMU in the bsp example is _even_ better. Build and run `esp-box/example` on ESP-BOX and ensure the IMU works by looking at the on-screen-display while moving the box around various orientations. * fleshing out some more * fix sa for imu * fleshing out examples and adding filters for computing pitch/roll from 6-axis imu * fix sa and update icm example some more * update example to support kconfig for custom hardware * udpate fast_inv_sqrt and affected examples / code * fix lib build * update filter * WIP updating example to work with madgwick filter as well * working madgwick filter * update comment * fix sa * only add compile options if not msvc * readme: update * update icm example to use madgwick filter as well * reimplement kalman filter to support n-dimensional state; update examples to use both madgwick and kalman filters and show both (text and line) * simplify code and update example * update to use std::tie * update docs
1 parent 01f4f15 commit 04e62fe

35 files changed

+2232
-30
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/base_peripheral/include/base_peripheral.hpp

+22
Original file line numberDiff line numberDiff line change
@@ -636,6 +636,28 @@ class BasePeripheral : public BaseComponent {
636636
write_u8_to_register(register_address, data, ec);
637637
}
638638

639+
/// Set bits in a register on the peripheral by mask
640+
/// \param register_address The address of the register to modify
641+
/// \param mask The mask to use when setting the bits. All bits not in the
642+
/// mask will be unmodified, while all bits within the mask will
643+
/// be set to the value of the corresponding bit in the value
644+
/// \param value The value to set. Bits in the value should correspond to the
645+
/// bits in the mask
646+
/// \param ec The error code to set if there is an error
647+
void set_bits_in_register_by_mask(RegisterAddressType register_address, uint8_t mask,
648+
uint8_t value, std::error_code &ec) {
649+
logger_.debug("set_bits_in_register_by_mask 0x{:x} with mask 0x{:x} and value 0x{:x}",
650+
register_address, mask, value);
651+
std::lock_guard<std::recursive_mutex> lock(base_mutex_);
652+
uint8_t data = read_u8_from_register(register_address, ec);
653+
if (ec) {
654+
return;
655+
}
656+
data &= ~mask;
657+
data |= value & mask;
658+
write_u8_to_register(register_address, data, ec);
659+
}
660+
639661
/// Clear bits in a register on the peripheral
640662
/// \param register_address The address of the register to modify
641663
/// \param mask The mask to clear

components/bldc_motor/include/bldc_motor.hpp

+3-2
Original file line numberDiff line numberDiff line change
@@ -407,8 +407,9 @@ class BldcMotor : public BaseComponent {
407407
float Uout;
408408
// a bit of optitmisation
409409
if (ud) { // only if ud and uq set
410-
// fast_sqrt is an approx of sqrt (3-4% error)
411-
Uout = fast_sqrt(ud * ud + uq * uq) / driver_->get_voltage_limit();
410+
// fast_inv_sqrt is an approx of invsqrt (3-4% error), so we need to
411+
// invert it to get the correct value (1/inv_sqrt = sqrt)
412+
Uout = 1.0f / (fast_inv_sqrt(ud * ud + uq * uq) * driver_->get_voltage_limit());
412413
// angle normalisation in between 0 and 2pi
413414
// only necessary if using fast_sin and fast_cos - approximation functions
414415
el_angle = normalize_angle(el_angle + atan2(uq, ud));

components/esp-box/CMakeLists.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
idf_component_register(
22
INCLUDE_DIRS "include"
33
SRC_DIRS "src"
4-
REQUIRES driver base_component codec display display_drivers i2c input_drivers interrupt gt911 task tt21100
4+
REQUIRES driver base_component codec display display_drivers i2c input_drivers interrupt gt911 task tt21100 icm42607
55
REQUIRED_IDF_TARGETS "esp32s3"
66
)

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/README.md

+16
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,13 @@ state and each time you touch the scren it 1) uses LVGL to draw a circle where
99
you touch, and 2) play a click sound (wav file bundled with the firmware). If
1010
you press the home button on the display, it will clear the circles.
1111

12+
https://github.com/user-attachments/assets/6fc9ce02-fdae-4f36-9ee7-3a6c347ca1c4
13+
1214
https://github.com/esp-cpp/espp/assets/213467/d5379983-9bc2-4d56-a9fc-9e37f54af15e
1315

1416
![image](https://github.com/esp-cpp/espp/assets/213467/bfb45218-0d1f-4a07-ba30-c0c74a1657b9)
1517
![image](https://github.com/esp-cpp/espp/assets/213467/c7216cfd-330d-4610-baf2-30001c98ff42)
18+
![image](https://github.com/user-attachments/assets/6d9901f1-f4fe-433a-b6d5-c8c82abd14b7)
1619

1720
## How to use example
1821

@@ -42,3 +45,16 @@ BOX3:
4245

4346
BOX:
4447
![CleanShot 2024-07-01 at 09 56 23](https://github.com/esp-cpp/espp/assets/213467/2f758ff5-a82e-4620-896e-99223010f013)
48+
49+
Slow rotation video:
50+
51+
https://github.com/user-attachments/assets/6fc9ce02-fdae-4f36-9ee7-3a6c347ca1c4
52+
53+
Fast rotation video:
54+
55+
https://github.com/user-attachments/assets/64fd1d71-d6b2-4c4f-8538-4097d4d162e9
56+
57+
Pictures showing the new gravity vector pointing down from the center of the screen:
58+
![image](https://github.com/user-attachments/assets/0a971b19-95b9-468c-8d5b-e99f2fbf76b9)
59+
![image](https://github.com/user-attachments/assets/6d9901f1-f4fe-433a-b6d5-c8c82abd14b7)
60+

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

+147-3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55

66
#include "esp-box.hpp"
77

8+
#include "kalman_filter.hpp"
9+
#include "madgwick_filter.hpp"
10+
811
using namespace std::chrono_literals;
912

1013
static constexpr size_t MAX_CIRCLES = 100;
@@ -80,16 +83,50 @@ extern "C" void app_main(void) {
8083
return;
8184
}
8285

86+
// initialize the IMU
87+
if (!box.initialize_imu()) {
88+
logger.error("Failed to initialize IMU!");
89+
return;
90+
}
91+
8392
// set the background color to black
8493
lv_obj_t *bg = lv_obj_create(lv_screen_active());
8594
lv_obj_set_size(bg, box.lcd_width(), box.lcd_height());
8695
lv_obj_set_style_bg_color(bg, lv_color_make(0, 0, 0), 0);
8796

8897
// add text in the center of the screen
8998
lv_obj_t *label = lv_label_create(lv_screen_active());
90-
lv_label_set_text(label, "Touch the screen!\nPress the home button to clear circles.");
91-
lv_obj_align(label, LV_ALIGN_CENTER, 0, 0);
92-
lv_obj_set_style_text_align(label, LV_TEXT_ALIGN_CENTER, 0);
99+
static std::string label_text =
100+
"\n\n\n\nTouch the screen!\nPress the home button to clear circles.";
101+
lv_label_set_text(label, label_text.c_str());
102+
lv_obj_align(label, LV_ALIGN_TOP_LEFT, 0, 0);
103+
lv_obj_set_style_text_align(label, LV_TEXT_ALIGN_LEFT, 0);
104+
105+
/*Create style*/
106+
static lv_style_t style_line0;
107+
lv_style_init(&style_line0);
108+
lv_style_set_line_width(&style_line0, 8);
109+
lv_style_set_line_color(&style_line0, lv_palette_main(LV_PALETTE_BLUE));
110+
lv_style_set_line_rounded(&style_line0, true);
111+
112+
// make a line for showing the direction of "down"
113+
lv_obj_t *line0 = lv_line_create(lv_screen_active());
114+
static lv_point_precise_t line_points0[] = {{0, 0}, {box.lcd_width(), box.lcd_height()}};
115+
lv_line_set_points(line0, line_points0, 2);
116+
lv_obj_add_style(line0, &style_line0, 0);
117+
118+
/*Create style*/
119+
static lv_style_t style_line1;
120+
lv_style_init(&style_line1);
121+
lv_style_set_line_width(&style_line1, 8);
122+
lv_style_set_line_color(&style_line1, lv_palette_main(LV_PALETTE_RED));
123+
lv_style_set_line_rounded(&style_line1, true);
124+
125+
// make a line for showing the direction of "down"
126+
lv_obj_t *line1 = lv_line_create(lv_screen_active());
127+
static lv_point_precise_t line_points1[] = {{0, 0}, {box.lcd_width(), box.lcd_height()}};
128+
lv_line_set_points(line1, line_points1, 2);
129+
lv_obj_add_style(line1, &style_line1, 0);
93130

94131
// add a button in the top left which (when pressed) will rotate the display
95132
// through 0, 90, 180, 270 degrees
@@ -129,6 +166,7 @@ extern "C" void app_main(void) {
129166
},
130167
.task_config = {
131168
.name = "lv_task",
169+
.stack_size_bytes = 6 * 1024,
132170
}});
133171
lv_task.start();
134172

@@ -143,6 +181,112 @@ extern "C" void app_main(void) {
143181
// set the display brightness to be 75%
144182
box.brightness(75.0f);
145183

184+
// make a task to read out the IMU data and print it to console
185+
espp::Task imu_task(
186+
{.callback = [&label, &line0, &line1](std::mutex &m, std::condition_variable &cv) -> bool {
187+
// sleep first in case we don't get IMU data and need to exit early
188+
{
189+
std::unique_lock<std::mutex> lock(m);
190+
cv.wait_for(lock, 10ms);
191+
}
192+
static auto &box = espp::EspBox::get();
193+
static auto imu = box.imu();
194+
195+
auto now = esp_timer_get_time(); // time in microseconds
196+
static auto t0 = now;
197+
auto t1 = now;
198+
float dt = (t1 - t0) / 1'000'000.0f; // convert us to s
199+
t0 = t1;
200+
201+
std::error_code ec;
202+
// get accel
203+
auto accel = imu->get_accelerometer(ec);
204+
if (ec) {
205+
return false;
206+
}
207+
auto gyro = imu->get_gyroscope(ec);
208+
if (ec) {
209+
return false;
210+
}
211+
auto temp = imu->get_temperature(ec);
212+
if (ec) {
213+
return false;
214+
}
215+
216+
// with only the accelerometer + gyroscope, we can't get yaw :(
217+
float roll = 0, pitch = 0;
218+
static constexpr float angle_noise = 0.001f;
219+
static constexpr float rate_noise = 0.1f;
220+
static espp::KalmanFilter<2> kf;
221+
kf.set_process_noise(rate_noise);
222+
kf.set_measurement_noise(angle_noise);
223+
static constexpr float beta = 0.1f; // higher = more accelerometer, lower = more gyro
224+
static espp::MadgwickFilter f(beta);
225+
226+
f.update(dt, accel.x, accel.y, accel.z, gyro.x * M_PI / 180.0f, gyro.y * M_PI / 180.0f,
227+
gyro.z * M_PI / 180.0f);
228+
float yaw; // ignore / unused since we only have 6-axis
229+
f.get_euler(roll, pitch, yaw);
230+
pitch *= M_PI / 180.0f;
231+
roll *= M_PI / 180.0f;
232+
233+
std::string text = fmt::format("{}\n\n\n\n\n", label_text);
234+
text += fmt::format("Accel: {:02.2f} {:02.2f} {:02.2f}\n", accel.x, accel.y, accel.z);
235+
text += fmt::format("Gyro: {:03.2f} {:03.2f} {:03.2f}\n", gyro.x * M_PI / 180.0f,
236+
gyro.y * M_PI / 180.0f, gyro.z * M_PI / 180.0f);
237+
text +=
238+
fmt::format("Angle: {:03.2f} {:03.2f}\n", roll * 180.0f / M_PI, pitch * 180.0f / M_PI);
239+
text += fmt::format("Temp: {:02.1f} C\n", temp);
240+
241+
// use the pitch to to draw a line on the screen indiating the
242+
// direction from the center of the screen to "down"
243+
int x0 = box.lcd_width() / 2;
244+
int y0 = box.lcd_height() / 2;
245+
246+
float vx = sin(pitch);
247+
float vy = -cos(pitch) * sin(roll);
248+
float vz = -cos(pitch) * cos(roll);
249+
250+
int x1 = x0 + 50 * vx;
251+
int y1 = y0 + 50 * vy;
252+
253+
static lv_point_precise_t line_points0[] = {{x0, y0}, {x1, y1}};
254+
line_points0[1].x = x1;
255+
line_points0[1].y = y1;
256+
257+
// Apply Kalman filter
258+
float accelPitch = atan2(-accel.x, sqrt(accel.y * accel.y + accel.z * accel.z));
259+
float accelRoll = atan2(accel.y, accel.z);
260+
kf.predict({float(gyro.x * M_PI / 180.0f), float(gyro.y * M_PI / 180.0f)}, dt);
261+
kf.update({accelPitch, accelRoll});
262+
std::tie(pitch, roll) = kf.get_state();
263+
264+
vx = sin(pitch);
265+
vy = -cos(pitch) * sin(roll);
266+
vz = -cos(pitch) * cos(roll);
267+
268+
x1 = x0 + 50 * vx;
269+
y1 = y0 + 50 * vy;
270+
271+
static lv_point_precise_t line_points1[] = {{x0, y0}, {x1, y1}};
272+
line_points1[1].x = x1;
273+
line_points1[1].y = y1;
274+
275+
std::lock_guard<std::recursive_mutex> lock(lvgl_mutex);
276+
lv_label_set_text(label, text.c_str());
277+
lv_line_set_points(line0, line_points0, 2);
278+
lv_line_set_points(line1, line_points1, 2);
279+
280+
return false;
281+
},
282+
.task_config = {
283+
.name = "IMU",
284+
.stack_size_bytes = 6 * 1024,
285+
.priority = 10,
286+
.core_id = 0,
287+
}});
288+
imu_task.start();
289+
146290
// loop forever
147291
while (true) {
148292
std::this_thread::sleep_for(1s);

components/esp-box/include/esp-box.hpp

+24
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#include "es8311.hpp"
2020
#include "gt911.hpp"
2121
#include "i2c.hpp"
22+
#include "icm42607.hpp"
2223
#include "interrupt.hpp"
2324
#include "st7789.hpp"
2425
#include "touchpad_input.hpp"
@@ -32,6 +33,9 @@ namespace espp {
3233
/// - Touchpad
3334
/// - Display
3435
/// - Audio
36+
/// - Interrupts
37+
/// - I2C
38+
/// - IMU (Inertial Measurement Unit)
3539
///
3640
/// The class is a singleton and can be accessed using the get() method.
3741
///
@@ -44,8 +48,13 @@ class EspBox : public BaseComponent {
4448

4549
/// Alias for the display driver used by the ESP-Box display
4650
using DisplayDriver = espp::St7789;
51+
52+
/// Alias for the touchpad data used by the ESP-Box touchpad
4753
using TouchpadData = espp::TouchpadData;
4854

55+
/// Alias the IMU used by the ESP-Box
56+
using Imu = espp::Icm42607<icm42607::Interface::I2C>;
57+
4958
/// The type of the box
5059
enum class BoxType {
5160
UNKNOWN, ///< unknown box
@@ -286,6 +295,18 @@ class EspBox : public BaseComponent {
286295
/// \param num_bytes The number of bytes to play
287296
void play_audio(const uint8_t *data, uint32_t num_bytes);
288297

298+
/////////////////////////////////////////////////////////////////////////////
299+
// IMU
300+
/////////////////////////////////////////////////////////////////////////////
301+
302+
/// Initialize the IMU
303+
/// \return true if the IMU was successfully initialized, false otherwise
304+
bool initialize_imu();
305+
306+
/// Get the IMU
307+
/// \return A shared pointer to the IMU
308+
std::shared_ptr<Imu> imu() const;
309+
289310
protected:
290311
EspBox();
291312
void detect();
@@ -440,6 +461,9 @@ class EspBox : public BaseComponent {
440461
i2s_std_config_t audio_std_cfg;
441462
i2s_event_callbacks_t audio_tx_callbacks_;
442463
std::atomic<bool> has_sound{false};
464+
465+
// IMU
466+
std::shared_ptr<Imu> imu_;
443467
}; // class EspBox
444468
} // namespace espp
445469

0 commit comments

Comments
 (0)