IMU Updates (#694)

* IMU Updates

Updated BMI270 and MPU6886 with the mount-matrix.
Updated devices i can test with the mount-matrix. papers3, stackchan (cores3), sticks3, tab5, waveshare-s3-lcd-13, waveshare-s3-lcd-128
Devices not changed and not tested because i don't own them are:

MPU6886 - core2, stickc-plus2.
BMI270 - cardputer-adv

* Update device.properties

* No USB here! CH343P
This commit is contained in:
Shadowtrance 2026-10-15 07:26:43 +10:00 committed by GitHub
parent 6a3c549324
commit d0ad1b014c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 118 additions and 126 deletions

View File

@ -65,7 +65,9 @@
bmi270 {
compatible = "bosch,bmi270";
reg = <0x69>;
invert-x;
mount-matrix = <1 0 0
0 1 0
0 0 1>;
};
bm8563 {

View File

@ -44,6 +44,9 @@
bmi270 {
compatible = "bosch,bmi270";
reg = <0x68>;
mount-matrix = <0 1 0
1 0 0
0 0 -1>;
};
bm8563 {

View File

@ -70,7 +70,9 @@
bmi270 {
compatible = "bosch,bmi270";
reg = <0x69>;
invert-x;
mount-matrix = <1 0 0
0 1 0
0 0 1>;
};
bm8563 {

View File

@ -63,6 +63,9 @@
bmi270 {
compatible = "bosch,bmi270";
reg = <0x68>;
mount-matrix = <0 1 0
-1 0 0
0 0 1>;
};
es8311 {

View File

@ -83,6 +83,9 @@
bmi270 {
compatible = "bosch,bmi270";
reg = <0x68>;
mount-matrix = <-1 0 0
0 1 0
0 0 -1>;
};
es8388 {

View File

@ -8,7 +8,6 @@ hardware.flashSize=16MB
hardware.spiRam=true
hardware.spiRamMode=OCT
hardware.spiRamSpeed=120M
hardware.tinyUsbMsc=true
hardware.esptoolFlashFreq=120M
hardware.bluetooth=true

View File

@ -9,7 +9,6 @@
#include <bindings/qmi8658.h>
#include <tactility/bindings/esp32_sdspi.h>
#include <tactility/bindings/esp32_pwm_ledc.h>
#include <tactility/bindings/esp32_usbdevice.h>
#include <tactility/bindings/pwm_backlight.h>
#include <bindings/st7789.h>
@ -41,6 +40,9 @@
qmi8658 {
compatible = "qst,qmi8658";
reg = <0x6B>;
mount-matrix = <-1 0 0
0 -1 0
0 0 -1>;
};
};
@ -89,12 +91,4 @@
frequency-khz = <20000>;
};
};
usbdevice0 {
compatible = "espressif,esp32-usbdevice";
usbdevicemsc0 {
compatible = "espressif,esp32-usbdevice-msc";
};
};
};

View File

@ -9,7 +9,6 @@ hardware.spiRam=true
hardware.spiRamMode=QUAD
hardware.spiRamSpeed=120M
hardware.spiRamXipDisabled=true
hardware.tinyUsbMsc=true
hardware.esptoolFlashFreq=120M
hardware.bluetooth=true

View File

@ -9,7 +9,6 @@
#include <bindings/qmi8658.h>
#include <tactility/bindings/esp32_sdspi.h>
#include <tactility/bindings/esp32_pwm_ledc.h>
#include <tactility/bindings/esp32_usbdevice.h>
#include <tactility/bindings/pwm_backlight.h>
#include <bindings/cst816t.h>
#include <bindings/gc9a01.h>
@ -42,6 +41,9 @@
qmi8658 {
compatible = "qst,qmi8658";
reg = <0x6B>;
mount-matrix = <0 1 0
1 0 0
0 0 1>;
};
touch0 {
@ -93,12 +95,4 @@
backlight = <&display_backlight>;
};
};
usbdevice0 {
compatible = "espressif,esp32-usbdevice";
usbdevicemsc0 {
compatible = "espressif,esp32-usbdevice-msc";
};
};
};

View File

@ -5,27 +5,11 @@ include: [ "i2c-device.yaml" ]
compatible: "bosch,bmi270"
properties:
swap-xy:
type: boolean
default: false
description: Swap the X and Y axes
swap-xz:
type: boolean
default: false
description: Swap the X and Z axes
swap-yz:
type: boolean
default: false
description: Swap the Y and Z axes
invert-x:
type: boolean
default: false
description: Invert the X axis
invert-y:
type: boolean
default: false
description: Invert the Y axis
invert-z:
type: boolean
default: false
description: Invert the Z axis
mount-matrix:
type: values
default: [1, 0, 0, 0, 1, 0, 0, 0, 1]
description: >
Row-major 3x3 rotation matrix that maps the sensor frame onto the device frame, following the
Linux IIO "mount-matrix" convention (x' = m[0]*x + m[1]*y + m[2]*z, and so on). It is applied
to the accelerometer and gyroscope readings so consumers always see the device frame,
regardless of how the IMU is physically oriented on the PCB. Defaults to the identity matrix.

View File

@ -14,12 +14,12 @@ extern "C" {
struct Bmi270Config {
/** Address on bus */
uint8_t address;
bool swap_xy;
bool swap_xz;
bool swap_yz;
bool invert_x;
bool invert_y;
bool invert_z;
/**
* Row-major 3x3 rotation matrix mapping the sensor frame onto the device frame (Linux IIO
* "mount-matrix" convention: `out[i] = m[i * 3] * x + m[i * 3 + 1] * y + m[i * 3 + 2] * z`).
* Identity means the sensor frame already matches the device frame.
*/
int32_t mount_matrix[9];
};
/**

View File

@ -5,8 +5,6 @@
#include <tactility/drivers/i2c_controller.h>
#include <tactility/log.h>
#include <algorithm>
#define TAG "BMI270"
static constexpr uint8_t REG_CHIP_ID = 0x00; // read: expect 0x24
@ -90,6 +88,18 @@ static bool configure(Device* i2c_controller, uint8_t address) {
// region Driver lifecycle
// Applies the devicetree mount matrix (sensor frame -> device frame) to a 3-axis reading.
// Flat row-major [9]: x' = m0*x + m1*y + m2*z, y' = m3*x + m4*y + m5*z, z' = m6*x + m7*y + m8*z.
static void applyMountMatrix(const Bmi270Config& config, float& x, float& y, float& z) {
const int32_t* m = config.mount_matrix;
const float rx = m[0] * x + m[1] * y + m[2] * z;
const float ry = m[3] * x + m[4] * y + m[5] * z;
const float rz = m[6] * x + m[7] * y + m[8] * z;
x = rx;
y = ry;
z = rz;
}
static error_t start(Device* device) {
auto* i2c_controller = device_get_parent(device);
if (device_get_type(i2c_controller) != &I2C_CONTROLLER_TYPE) {
@ -171,13 +181,7 @@ error_t bmi270_read_accel(Device* device, ImuAccelData* data) {
toI16(buffer[2], buffer[3]) * ACCEL_SCALE,
toI16(buffer[4], buffer[5]) * ACCEL_SCALE,
};
const auto& config = *GET_CONFIG(device);
if (config.swap_xy) std::swap(axes[0], axes[1]);
if (config.swap_xz) std::swap(axes[0], axes[2]);
if (config.swap_yz) std::swap(axes[1], axes[2]);
if (config.invert_x) axes[0] = -axes[0];
if (config.invert_y) axes[1] = -axes[1];
if (config.invert_z) axes[2] = -axes[2];
applyMountMatrix(*GET_CONFIG(device), axes[0], axes[1], axes[2]);
data->ax = axes[0];
data->ay = axes[1];
data->az = axes[2];
@ -197,9 +201,13 @@ error_t bmi270_read_gyro(Device* device, ImuGyroData* data) {
return static_cast<int16_t>(static_cast<uint16_t>(hi) << 8 | lo);
};
data->gx = toI16(buffer[0], buffer[1]) * GYRO_SCALE;
data->gy = toI16(buffer[2], buffer[3]) * GYRO_SCALE;
data->gz = toI16(buffer[4], buffer[5]) * GYRO_SCALE;
float gx = toI16(buffer[0], buffer[1]) * GYRO_SCALE;
float gy = toI16(buffer[2], buffer[3]) * GYRO_SCALE;
float gz = toI16(buffer[4], buffer[5]) * GYRO_SCALE;
applyMountMatrix(*GET_CONFIG(device), gx, gy, gz);
data->gx = gx;
data->gy = gy;
data->gz = gz;
return ERROR_NONE;
}

View File

@ -5,27 +5,11 @@ include: [ "i2c-device.yaml" ]
compatible: "invensense,mpu6886"
properties:
swap-xy:
type: boolean
default: false
description: Swap the X and Y axes
swap-xz:
type: boolean
default: false
description: Swap the X and Z axes
swap-yz:
type: boolean
default: false
description: Swap the Y and Z axes
invert-x:
type: boolean
default: false
description: Invert the X axis
invert-y:
type: boolean
default: false
description: Invert the Y axis
invert-z:
type: boolean
default: false
description: Invert the Z axis
mount-matrix:
type: values
default: [1, 0, 0, 0, 1, 0, 0, 0, 1]
description: >
Row-major 3x3 rotation matrix that maps the sensor frame onto the device frame, following the
Linux IIO "mount-matrix" convention (x' = m[0]*x + m[1]*y + m[2]*z, and so on). It is applied
to the accelerometer and gyroscope readings so consumers always see the device frame,
regardless of how the IMU is physically oriented on the PCB. Defaults to the identity matrix.

View File

@ -14,12 +14,12 @@ extern "C" {
struct Mpu6886Config {
/** Address on bus */
uint8_t address;
bool swap_xy;
bool swap_xz;
bool swap_yz;
bool invert_x;
bool invert_y;
bool invert_z;
/**
* Row-major 3x3 rotation matrix mapping the sensor frame onto the device frame (Linux IIO
* "mount-matrix" convention: `out[i] = m[i * 3] * x + m[i * 3 + 1] * y + m[i * 3 + 2] * z`).
* Identity means the sensor frame already matches the device frame.
*/
int32_t mount_matrix[9];
};
/**

View File

@ -4,8 +4,6 @@
#include <tactility/drivers/i2c_controller.h>
#include <tactility/log.h>
#include <algorithm>
#define TAG "MPU6886"
// Register map
@ -48,6 +46,18 @@ static constexpr TickType_t I2C_TIMEOUT_TICKS = pdMS_TO_TICKS(10);
// region Driver lifecycle
// Applies the devicetree mount matrix (sensor frame -> device frame) to a 3-axis reading.
// Flat row-major [9]: x' = m0*x + m1*y + m2*z, y' = m3*x + m4*y + m5*z, z' = m6*x + m7*y + m8*z.
static void applyMountMatrix(const Mpu6886Config& config, float& x, float& y, float& z) {
const int32_t* m = config.mount_matrix;
const float rx = m[0] * x + m[1] * y + m[2] * z;
const float ry = m[3] * x + m[4] * y + m[5] * z;
const float rz = m[6] * x + m[7] * y + m[8] * z;
x = rx;
y = ry;
z = rz;
}
static error_t start(Device* device) {
auto* i2c_controller = device_get_parent(device);
if (device_get_type(i2c_controller) != &I2C_CONTROLLER_TYPE) {
@ -145,13 +155,7 @@ error_t mpu6886_read_accel(Device* device, ImuAccelData* data) {
toI16(buf[2], buf[3]) * ACCEL_SCALE,
toI16(buf[4], buf[5]) * ACCEL_SCALE,
};
const auto& config = *GET_CONFIG(device);
if (config.swap_xy) std::swap(axes[0], axes[1]);
if (config.swap_xz) std::swap(axes[0], axes[2]);
if (config.swap_yz) std::swap(axes[1], axes[2]);
if (config.invert_x) axes[0] = -axes[0];
if (config.invert_y) axes[1] = -axes[1];
if (config.invert_z) axes[2] = -axes[2];
applyMountMatrix(*GET_CONFIG(device), axes[0], axes[1], axes[2]);
data->ax = axes[0];
data->ay = axes[1];
data->az = axes[2];
@ -171,9 +175,13 @@ error_t mpu6886_read_gyro(Device* device, ImuGyroData* data) {
error_t error = i2c_controller_read_register(i2c_controller, address, REG_GYRO_XOUT_H, buf, sizeof(buf), I2C_TIMEOUT_TICKS);
if (error != ERROR_NONE) return error;
data->gx = toI16(buf[0], buf[1]) * GYRO_SCALE;
data->gy = toI16(buf[2], buf[3]) * GYRO_SCALE;
data->gz = toI16(buf[4], buf[5]) * GYRO_SCALE;
float gx = toI16(buf[0], buf[1]) * GYRO_SCALE;
float gy = toI16(buf[2], buf[3]) * GYRO_SCALE;
float gz = toI16(buf[4], buf[5]) * GYRO_SCALE;
applyMountMatrix(*GET_CONFIG(device), gx, gy, gz);
data->gx = gx;
data->gy = gy;
data->gz = gz;
return ERROR_NONE;
}

View File

@ -343,6 +343,10 @@ const struct ModuleSymbol lvgl_module_symbols[] = {
DEFINE_MODULE_SYMBOL(lv_obj_set_style_height),
DEFINE_MODULE_SYMBOL(lv_obj_set_style_clip_corner),
DEFINE_MODULE_SYMBOL(lv_obj_set_style_shadow_color),
DEFINE_MODULE_SYMBOL(lv_obj_set_parent),
DEFINE_MODULE_SYMBOL(lv_obj_set_style_bg_image_tiled),
DEFINE_MODULE_SYMBOL(lv_obj_set_style_image_recolor),
DEFINE_MODULE_SYMBOL(lv_obj_set_style_image_opa),
// lv_font
DEFINE_MODULE_SYMBOL(lv_font_get_bitmap_fmt_txt),
DEFINE_MODULE_SYMBOL(lv_font_get_default),

View File

@ -84,7 +84,7 @@ struct Context {
lv_obj_t* timeoutValueLabel = nullptr;
lv_obj_t* screensaverValueLabel = nullptr;
// Shown when auto-rotate is off or unavailable: the orientation label and chips
lv_obj_t* orientationGroup = nullptr;
lv_obj_t* orientationCard = nullptr;
lv_obj_t* orientationChips = nullptr;
lv_obj_t* autoRotateSwitch = nullptr;
};
@ -151,10 +151,10 @@ void setCheckedChip(lv_obj_t* chips, settings::display::Orientation orientation)
});
}
/** Shows or hides the manual orientation chips, depending on whether auto-rotate is on */
/** Shows or hides the manual orientation card, depending on whether auto-rotate is on */
void updateOrientationVisibility(Context* ctx) {
const bool auto_rotate = ctx->autoRotateSwitch != nullptr && ctx->displaySettings.autoRotateEnabled;
lv_obj_set_hidden(ctx->orientationGroup, auto_rotate);
lv_obj_set_hidden(ctx->orientationCard, auto_rotate);
}
void onOrientationChipPressed(lv_event_t* event) {
@ -396,12 +396,12 @@ void createWidgets(lv_obj_t* parent, void* userData) {
}
// The card shows the manual orientation; auto-rotate hides it while it drives the rotation
auto* orientation_card = lvgl_card_create(main_wrapper);
lv_obj_set_size(orientation_card, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_flex_flow(orientation_card, LV_FLEX_FLOW_COLUMN);
ctx->orientationCard = lvgl_card_create(main_wrapper);
lv_obj_set_size(ctx->orientationCard, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_flex_flow(ctx->orientationCard, LV_FLEX_FLOW_COLUMN);
ctx->orientationGroup = createCardGroup(orientation_card, "Orientation");
ctx->orientationChips = createOrientationChips(ctx->orientationGroup, ctx->displaySettings.orientation, onOrientationChipPressed, ctx);
auto* orientation_group = createCardGroup(ctx->orientationCard, "Orientation");
ctx->orientationChips = createOrientationChips(orientation_group, ctx->displaySettings.orientation, onOrientationChipPressed, ctx);
updateOrientationVisibility(ctx);
@ -447,7 +447,7 @@ void destroyWidgets(void* userData) {
ctx->timeoutCard = nullptr;
ctx->timeoutValueLabel = nullptr;
ctx->screensaverValueLabel = nullptr;
ctx->orientationGroup = nullptr;
ctx->orientationCard = nullptr;
ctx->orientationChips = nullptr;
ctx->autoRotateSwitch = nullptr;
}

View File

@ -7,7 +7,10 @@ namespace tt::service::autorotate {
constexpr float FLAT_THRESHOLD = 0.45F; // gravity in the screen plane below this = lying flat
constexpr int DEBOUNCE = 5; // consecutive readings before switching
// gx toward +Y -> rotation 0; each +90 degrees of gx/gy angle is one more quarter-turn.
// ax/ay is the accelerometer support-force vector (points up, opposite gravity).
// The UI top should point along (ax,ay): +Y -> rotation 0.
// LVGL ROT90 is 90 deg counter-clockwise (UI top -> device left, -X), ROT270 is
// 90 deg clockwise (UI top -> device right, +X), so +X maps to 270, not 90.
static lv_display_rotation_t gravityToRotation(float gx, float gy, lv_display_rotation_t fallback) {
float plane = sqrtf((gx * gx) + (gy * gy));
if (plane < FLAT_THRESHOLD) {
@ -22,9 +25,9 @@ static lv_display_rotation_t gravityToRotation(float gx, float gy, lv_display_ro
int idx = (int)((ang + 45.0F) / 90.0F) % 4;
static constexpr lv_display_rotation_t map[4] = {
LV_DISPLAY_ROTATION_0,
LV_DISPLAY_ROTATION_90,
LV_DISPLAY_ROTATION_180,
LV_DISPLAY_ROTATION_270,
LV_DISPLAY_ROTATION_180,
LV_DISPLAY_ROTATION_90,
};
return map[idx];
}

View File

@ -9,7 +9,7 @@ TEST_CASE("a flat reading below the threshold keeps the current rotation") {
CHECK_EQ(math.update(0.1F, 0.1F), LV_DISPLAY_ROTATION_0);
}
TEST_CASE("gravity toward +X classifies as rotation 90 when no debounce is needed yet") {
TEST_CASE("accel toward +X classifies as rotation 270 when no debounce is needed yet") {
OrientationMath math;
math.reset(LV_DISPLAY_ROTATION_0);
// A single reading only sets the debounce target - the previous rotation is still returned.
@ -27,32 +27,34 @@ TEST_CASE("a new rotation is only applied after enough consecutive stable readin
CHECK_EQ(last, LV_DISPLAY_ROTATION_0); // not yet debounced
}
last = math.update(1.0F, 0.0F);
CHECK_EQ(last, LV_DISPLAY_ROTATION_90); // 6th call (5th consecutive stable reading) switches
CHECK_EQ(last, LV_DISPLAY_ROTATION_270); // 6th call (5th consecutive stable reading) switches
}
TEST_CASE("all four gravity directions map to the expected rotation") {
TEST_CASE("all four accel directions map to the expected rotation") {
OrientationMath math;
math.reset(LV_DISPLAY_ROTATION_0);
auto settle = [&](float gx, float gy) {
auto settle = [&](float ax, float ay) {
lv_display_rotation_t result = LV_DISPLAY_ROTATION_0;
for (int i = 0; i < 6; i++) {
result = math.update(gx, gy);
result = math.update(ax, ay);
}
return result;
};
// Accel points up (opposite gravity); UI top follows it.
// LVGL ROT90 is CCW (top -> -X), ROT270 is CW (top -> +X).
CHECK_EQ(settle(0.0F, 1.0F), LV_DISPLAY_ROTATION_0);
CHECK_EQ(settle(1.0F, 0.0F), LV_DISPLAY_ROTATION_90);
CHECK_EQ(settle(1.0F, 0.0F), LV_DISPLAY_ROTATION_270);
CHECK_EQ(settle(0.0F, -1.0F), LV_DISPLAY_ROTATION_180);
CHECK_EQ(settle(-1.0F, 0.0F), LV_DISPLAY_ROTATION_270);
CHECK_EQ(settle(-1.0F, 0.0F), LV_DISPLAY_ROTATION_90);
}
TEST_CASE("an unstable target resets the debounce counter") {
OrientationMath math;
math.reset(LV_DISPLAY_ROTATION_0);
// Get close to switching to rotation 90...
// Get close to switching to rotation 270...
for (int i = 0; i < 4; i++) {
math.update(1.0F, 0.0F);
}