diff --git a/Data/data/webserver/dashboard.html b/Data/data/webserver/dashboard.html
index d964ae23b..69553e720 100644
--- a/Data/data/webserver/dashboard.html
+++ b/Data/data/webserver/dashboard.html
@@ -613,10 +613,12 @@
+ ${dataPartition.mounted ? `
` : ''}
SD Card
- ${
- data.storage.sdcard.mounted ? formatBytes(data.storage.sdcard.free) + ' free' : 'Not mounted'
+ ${
+ sdcard.mounted ? formatBytes(sdcard.free) + ' free' : 'Not mounted'
}
- ${data.storage.sdcard.mounted ? `
+ ${sdcard.mounted ? `
` : ''}
diff --git a/Devices/btt-panda-touch/device.properties b/Devices/btt-panda-touch/device.properties
index 1f34fcb6a..a1d965ae6 100644
--- a/Devices/btt-panda-touch/device.properties
+++ b/Devices/btt-panda-touch/device.properties
@@ -19,4 +19,5 @@ shape=rectangle
dpi=187
[lvgl]
-colorDepth=16
\ No newline at end of file
+colorDepth=16
+fontSize=18
diff --git a/Devices/cyd-8048s043c/device.properties b/Devices/cyd-8048s043c/device.properties
index 6c6a231ee..80114bee8 100644
--- a/Devices/cyd-8048s043c/device.properties
+++ b/Devices/cyd-8048s043c/device.properties
@@ -26,3 +26,4 @@ warningMessage=
[lvgl]
theme=DefaultDark
colorDepth=16
+fontSize=18
diff --git a/Devices/elecrow-crowpanel-advance-50/device.properties b/Devices/elecrow-crowpanel-advance-50/device.properties
index 00780ad28..f12aba6f0 100644
--- a/Devices/elecrow-crowpanel-advance-50/device.properties
+++ b/Devices/elecrow-crowpanel-advance-50/device.properties
@@ -21,3 +21,4 @@ dpi=187
[lvgl]
colorDepth=16
+fontSize=18
diff --git a/Devices/elecrow-crowpanel-basic-50/device.properties b/Devices/elecrow-crowpanel-basic-50/device.properties
index 4ccf0fdaf..6372dc6b6 100644
--- a/Devices/elecrow-crowpanel-basic-50/device.properties
+++ b/Devices/elecrow-crowpanel-basic-50/device.properties
@@ -21,3 +21,4 @@ dpi=187
[lvgl]
colorDepth=16
+fontSize=18
diff --git a/Devices/guition-jc8048w550c/device.properties b/Devices/guition-jc8048w550c/device.properties
index 0a50b3f0c..e405ed7b6 100644
--- a/Devices/guition-jc8048w550c/device.properties
+++ b/Devices/guition-jc8048w550c/device.properties
@@ -20,3 +20,4 @@ dpi=187
[lvgl]
colorDepth=16
+fontSize=18
diff --git a/Devices/m5stack-cardputer-adv/devicetree.yaml b/Devices/m5stack-cardputer-adv/devicetree.yaml
index 2548e656e..d6b380731 100644
--- a/Devices/m5stack-cardputer-adv/devicetree.yaml
+++ b/Devices/m5stack-cardputer-adv/devicetree.yaml
@@ -1,3 +1,4 @@
dependencies:
- - Platforms/platform-esp32
+- Platforms/platform-esp32
+- Drivers/bmi270-module
dts: m5stack,cardputer-adv.dts
diff --git a/Devices/m5stack-cardputer-adv/m5stack,cardputer-adv.dts b/Devices/m5stack-cardputer-adv/m5stack,cardputer-adv.dts
index b51c37b16..421f3436a 100644
--- a/Devices/m5stack-cardputer-adv/m5stack,cardputer-adv.dts
+++ b/Devices/m5stack-cardputer-adv/m5stack,cardputer-adv.dts
@@ -6,6 +6,7 @@
#include
#include
#include
+#include
// Reference: https://docs.m5stack.com/en/core/Cardputer-Adv
/ {
@@ -23,6 +24,11 @@
clock-frequency = <400000>;
pin-sda = <&gpio0 8 GPIO_FLAG_NONE>;
pin-scl = <&gpio0 9 GPIO_FLAG_NONE>;
+
+ bmi270 {
+ compatible = "bosch,bmi270";
+ reg = <0x68>;
+ };
};
i2c_port_a {
diff --git a/Devices/m5stack-core2/devicetree.yaml b/Devices/m5stack-core2/devicetree.yaml
index 3016b01ab..d1c990164 100644
--- a/Devices/m5stack-core2/devicetree.yaml
+++ b/Devices/m5stack-core2/devicetree.yaml
@@ -1,3 +1,5 @@
dependencies:
- - Platforms/platform-esp32
+- Platforms/platform-esp32
+- Drivers/mpu6886-module
+- Drivers/bm8563-module
dts: m5stack,core2.dts
diff --git a/Devices/m5stack-core2/m5stack,core2.dts b/Devices/m5stack-core2/m5stack,core2.dts
index f18fd6e24..8092e39cd 100644
--- a/Devices/m5stack-core2/m5stack,core2.dts
+++ b/Devices/m5stack-core2/m5stack,core2.dts
@@ -6,6 +6,8 @@
#include
#include
#include
+#include
+#include
// Reference: https://docs.m5stack.com/en/core/Core2
/ {
@@ -23,6 +25,16 @@
clock-frequency = <400000>;
pin-sda = <&gpio0 21 GPIO_FLAG_NONE>;
pin-scl = <&gpio0 22 GPIO_FLAG_NONE>;
+
+ mpu6886 {
+ compatible = "invensense,mpu6886";
+ reg = <0x68>;
+ };
+
+ bm8563 {
+ compatible = "belling,bm8563";
+ reg = <0x51>;
+ };
};
i2c_port_a {
diff --git a/Devices/m5stack-cores3/devicetree.yaml b/Devices/m5stack-cores3/devicetree.yaml
index daa64904a..b65b1d7e5 100644
--- a/Devices/m5stack-cores3/devicetree.yaml
+++ b/Devices/m5stack-cores3/devicetree.yaml
@@ -1,3 +1,5 @@
dependencies:
- - Platforms/platform-esp32
+- Platforms/platform-esp32
+- Drivers/bmi270-module
+- Drivers/bm8563-module
dts: m5stack,cores3.dts
diff --git a/Devices/m5stack-cores3/m5stack,cores3.dts b/Devices/m5stack-cores3/m5stack,cores3.dts
index 55adccc13..08b17fe96 100644
--- a/Devices/m5stack-cores3/m5stack,cores3.dts
+++ b/Devices/m5stack-cores3/m5stack,cores3.dts
@@ -6,6 +6,8 @@
#include
#include
#include
+#include
+#include
// Reference: https://docs.m5stack.com/en/core/CoreS3
/ {
@@ -23,6 +25,16 @@
clock-frequency = <400000>;
pin-sda = <&gpio0 12 GPIO_FLAG_NONE>;
pin-scl = <&gpio0 11 GPIO_FLAG_NONE>;
+
+ bmi270 {
+ compatible = "bosch,bmi270";
+ reg = <0x68>;
+ };
+
+ bm8563 {
+ compatible = "belling,bm8563";
+ reg = <0x51>;
+ };
};
i2c_port_a {
diff --git a/Devices/m5stack-papers3/devicetree.yaml b/Devices/m5stack-papers3/devicetree.yaml
index e1585b29d..855cc0553 100644
--- a/Devices/m5stack-papers3/devicetree.yaml
+++ b/Devices/m5stack-papers3/devicetree.yaml
@@ -1,3 +1,5 @@
dependencies:
- - Platforms/platform-esp32
+- Platforms/platform-esp32
+- Drivers/bmi270-module
+- Drivers/bm8563-module
dts: m5stack,papers3.dts
diff --git a/Devices/m5stack-papers3/m5stack,papers3.dts b/Devices/m5stack-papers3/m5stack,papers3.dts
index 3d8b69549..207b351c2 100644
--- a/Devices/m5stack-papers3/m5stack,papers3.dts
+++ b/Devices/m5stack-papers3/m5stack,papers3.dts
@@ -4,6 +4,8 @@
#include
#include
#include
+#include
+#include
/ {
compatible = "root";
@@ -20,6 +22,16 @@
clock-frequency = <400000>;
pin-sda = <&gpio0 41 GPIO_FLAG_NONE>;
pin-scl = <&gpio0 42 GPIO_FLAG_NONE>;
+
+ bmi270 {
+ compatible = "bosch,bmi270";
+ reg = <0x68>;
+ };
+
+ bm8563 {
+ compatible = "belling,bm8563";
+ reg = <0x51>;
+ };
};
spi0 {
diff --git a/Devices/m5stack-stickc-plus/devicetree.yaml b/Devices/m5stack-stickc-plus/devicetree.yaml
index dcb567e86..287869e9c 100644
--- a/Devices/m5stack-stickc-plus/devicetree.yaml
+++ b/Devices/m5stack-stickc-plus/devicetree.yaml
@@ -1,3 +1,5 @@
dependencies:
- - Platforms/platform-esp32
+- Platforms/platform-esp32
+- Drivers/mpu6886-module
+- Drivers/bm8563-module
dts: m5stack,stickc-plus.dts
diff --git a/Devices/m5stack-stickc-plus/m5stack,stickc-plus.dts b/Devices/m5stack-stickc-plus/m5stack,stickc-plus.dts
index 88d618218..55bdbf6e7 100644
--- a/Devices/m5stack-stickc-plus/m5stack,stickc-plus.dts
+++ b/Devices/m5stack-stickc-plus/m5stack,stickc-plus.dts
@@ -5,6 +5,8 @@
#include
#include
#include
+#include
+#include
/ {
compatible = "root";
@@ -21,6 +23,16 @@
clock-frequency = <400000>;
pin-sda = <&gpio0 21 GPIO_FLAG_NONE>;
pin-scl = <&gpio0 22 GPIO_FLAG_NONE>;
+
+ mpu6886 {
+ compatible = "invensense,mpu6886";
+ reg = <0x68>;
+ };
+
+ bm8563 {
+ compatible = "belling,bm8563";
+ reg = <0x51>;
+ };
};
i2c_grove {
diff --git a/Devices/m5stack-stickc-plus2/devicetree.yaml b/Devices/m5stack-stickc-plus2/devicetree.yaml
index 7d3045f09..13b717386 100644
--- a/Devices/m5stack-stickc-plus2/devicetree.yaml
+++ b/Devices/m5stack-stickc-plus2/devicetree.yaml
@@ -1,3 +1,5 @@
dependencies:
- - Platforms/platform-esp32
+- Platforms/platform-esp32
+- Drivers/mpu6886-module
+- Drivers/bm8563-module
dts: m5stack,stickc-plus2.dts
diff --git a/Devices/m5stack-stickc-plus2/m5stack,stickc-plus2.dts b/Devices/m5stack-stickc-plus2/m5stack,stickc-plus2.dts
index 96524067d..9f86c6800 100644
--- a/Devices/m5stack-stickc-plus2/m5stack,stickc-plus2.dts
+++ b/Devices/m5stack-stickc-plus2/m5stack,stickc-plus2.dts
@@ -4,6 +4,8 @@
#include
#include
#include
+#include
+#include
/ {
compatible = "root";
@@ -20,6 +22,16 @@
clock-frequency = <400000>;
pin-sda = <&gpio0 21 GPIO_FLAG_NONE>;
pin-scl = <&gpio0 22 GPIO_FLAG_NONE>;
+
+ mpu6886 {
+ compatible = "invensense,mpu6886";
+ reg = <0x68>;
+ };
+
+ bm8563 {
+ compatible = "belling,bm8563";
+ reg = <0x51>;
+ };
};
i2c_grove {
diff --git a/Devices/m5stack-sticks3/CMakeLists.txt b/Devices/m5stack-sticks3/CMakeLists.txt
new file mode 100644
index 000000000..a6e098d03
--- /dev/null
+++ b/Devices/m5stack-sticks3/CMakeLists.txt
@@ -0,0 +1,7 @@
+file(GLOB_RECURSE SOURCE_FILES Source/*.c*)
+
+idf_component_register(
+ SRCS ${SOURCE_FILES}
+ INCLUDE_DIRS "Source"
+ REQUIRES Tactility esp_lvgl_port esp_lcd ST7789 PwmBacklight ButtonControl m5pm1-module
+)
diff --git a/Devices/m5stack-sticks3/Source/Configuration.cpp b/Devices/m5stack-sticks3/Source/Configuration.cpp
new file mode 100644
index 000000000..4c3619234
--- /dev/null
+++ b/Devices/m5stack-sticks3/Source/Configuration.cpp
@@ -0,0 +1,26 @@
+#include "devices/Display.h"
+#include "devices/Power.h"
+#include
+
+#include
+#include
+#include
+
+using namespace tt::hal;
+
+bool initBoot() {
+ return driver::pwmbacklight::init(GPIO_NUM_38, 512);
+}
+
+static DeviceVector createDevices() {
+ return {
+ createPower(),
+ ButtonControl::createTwoButtonControl(11, 12), // top button, side button
+ createDisplay()
+ };
+}
+
+extern const Configuration hardwareConfiguration = {
+ .initBoot = initBoot,
+ .createDevices = createDevices
+};
diff --git a/Devices/m5stack-sticks3/Source/devices/Display.cpp b/Devices/m5stack-sticks3/Source/devices/Display.cpp
new file mode 100644
index 000000000..5b732b068
--- /dev/null
+++ b/Devices/m5stack-sticks3/Source/devices/Display.cpp
@@ -0,0 +1,32 @@
+#include "Display.h"
+
+#include
+#include
+
+std::shared_ptr createDisplay() {
+ St7789Display::Configuration panel_configuration = {
+ .horizontalResolution = LCD_HORIZONTAL_RESOLUTION,
+ .verticalResolution = LCD_VERTICAL_RESOLUTION,
+ .gapX = 52,
+ .gapY = 40,
+ .swapXY = false,
+ .mirrorX = false,
+ .mirrorY = false,
+ .invertColor = true,
+ .bufferSize = LCD_BUFFER_SIZE,
+ .touch = nullptr,
+ .backlightDutyFunction = driver::pwmbacklight::setBacklightDuty,
+ .resetPin = LCD_PIN_RESET,
+ .lvglSwapBytes = false
+ };
+
+ auto spi_configuration = std::make_shared(St7789Display::SpiConfiguration {
+ .spiHostDevice = LCD_SPI_HOST,
+ .csPin = LCD_PIN_CS,
+ .dcPin = LCD_PIN_DC,
+ .pixelClockFrequency = 40'000'000,
+ .transactionQueueDepth = 10
+ });
+
+ return std::make_shared(panel_configuration, spi_configuration);
+}
diff --git a/Devices/m5stack-sticks3/Source/devices/Display.h b/Devices/m5stack-sticks3/Source/devices/Display.h
new file mode 100644
index 000000000..65142cb5f
--- /dev/null
+++ b/Devices/m5stack-sticks3/Source/devices/Display.h
@@ -0,0 +1,17 @@
+#pragma once
+
+#include "Tactility/hal/display/DisplayDevice.h"
+#include
+#include
+#include
+
+constexpr auto LCD_SPI_HOST = SPI2_HOST;
+constexpr auto LCD_PIN_CS = GPIO_NUM_41;
+constexpr auto LCD_PIN_DC = GPIO_NUM_45;
+constexpr auto LCD_PIN_RESET = GPIO_NUM_21;
+constexpr auto LCD_HORIZONTAL_RESOLUTION = 135;
+constexpr auto LCD_VERTICAL_RESOLUTION = 240;
+constexpr auto LCD_BUFFER_HEIGHT = LCD_VERTICAL_RESOLUTION / 3;
+constexpr auto LCD_BUFFER_SIZE = LCD_HORIZONTAL_RESOLUTION * LCD_BUFFER_HEIGHT;
+
+std::shared_ptr createDisplay();
diff --git a/Devices/m5stack-sticks3/Source/devices/Power.cpp b/Devices/m5stack-sticks3/Source/devices/Power.cpp
new file mode 100644
index 000000000..036b7595f
--- /dev/null
+++ b/Devices/m5stack-sticks3/Source/devices/Power.cpp
@@ -0,0 +1,94 @@
+#include "Power.h"
+
+#include
+#include
+#include
+#include
+
+using namespace tt::hal::power;
+
+static constexpr auto* TAG = "StickS3Power";
+
+static constexpr float MIN_BATTERY_VOLTAGE_MV = 3300.0f;
+static constexpr float MAX_BATTERY_VOLTAGE_MV = 4200.0f;
+
+class StickS3Power final : public PowerDevice {
+public:
+ explicit StickS3Power(::Device* m5pm1Device) : m5pm1(m5pm1Device) {}
+
+ std::string getName() const override { return "M5Stack StickS3 Power"; }
+ std::string getDescription() const override { return "Battery monitoring via M5PM1 over I2C"; }
+
+ bool supportsMetric(MetricType type) const override {
+ switch (type) {
+ using enum MetricType;
+ case BatteryVoltage:
+ case ChargeLevel:
+ case IsCharging:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ bool getMetric(MetricType type, MetricData& data) override {
+ switch (type) {
+ using enum MetricType;
+
+ case BatteryVoltage: {
+ uint16_t mv = 0;
+ if (m5pm1_get_battery_voltage(m5pm1, &mv) != ERROR_NONE) return false;
+ data.valueAsUint32 = mv;
+ return true;
+ }
+
+ case ChargeLevel: {
+ uint16_t mv = 0;
+ if (m5pm1_get_battery_voltage(m5pm1, &mv) != ERROR_NONE) return false;
+ float voltage = static_cast(mv);
+ if (voltage >= MAX_BATTERY_VOLTAGE_MV) {
+ data.valueAsUint8 = 100;
+ } else if (voltage <= MIN_BATTERY_VOLTAGE_MV) {
+ data.valueAsUint8 = 0;
+ } else {
+ float factor = (voltage - MIN_BATTERY_VOLTAGE_MV) / (MAX_BATTERY_VOLTAGE_MV - MIN_BATTERY_VOLTAGE_MV);
+ data.valueAsUint8 = static_cast(factor * 100.0f);
+ }
+ return true;
+ }
+
+ case IsCharging: {
+ bool charging = false;
+ if (m5pm1_is_charging(m5pm1, &charging) != ERROR_NONE) {
+ LOG_W(TAG, "Failed to read charging status");
+ return false;
+ }
+ data.valueAsBool = charging;
+ return true;
+ }
+
+ default:
+ return false;
+ }
+ }
+
+ bool supportsPowerOff() const override { return true; }
+
+ void powerOff() override {
+ LOG_W(TAG, "Powering off via M5PM1");
+ if (m5pm1_shutdown(m5pm1) != ERROR_NONE) {
+ LOG_E(TAG, "Failed to send power-off command");
+ }
+ }
+
+private:
+ ::Device* m5pm1;
+};
+
+std::shared_ptr createPower() {
+ auto* m5pm1 = device_find_by_name("m5pm1");
+ if (m5pm1 == nullptr) {
+ LOG_E(TAG, "m5pm1 device not found");
+ }
+ return std::make_shared(m5pm1);
+}
diff --git a/Devices/m5stack-sticks3/Source/devices/Power.h b/Devices/m5stack-sticks3/Source/devices/Power.h
new file mode 100644
index 000000000..7598ded6f
--- /dev/null
+++ b/Devices/m5stack-sticks3/Source/devices/Power.h
@@ -0,0 +1,6 @@
+#pragma once
+
+#include
+#include
+
+std::shared_ptr createPower();
diff --git a/Devices/m5stack-sticks3/Source/module.cpp b/Devices/m5stack-sticks3/Source/module.cpp
new file mode 100644
index 000000000..77b30f3c2
--- /dev/null
+++ b/Devices/m5stack-sticks3/Source/module.cpp
@@ -0,0 +1,23 @@
+#include
+
+extern "C" {
+
+static error_t start() {
+ // Empty for now
+ return ERROR_NONE;
+}
+
+static error_t stop() {
+ // Empty for now
+ return ERROR_NONE;
+}
+
+struct Module m5stack_sticks3_module = {
+ .name = "m5stack-sticks3",
+ .start = start,
+ .stop = stop,
+ .symbols = nullptr,
+ .internal = nullptr
+};
+
+}
diff --git a/Devices/m5stack-sticks3/device.properties b/Devices/m5stack-sticks3/device.properties
new file mode 100644
index 000000000..4bcc3b9e3
--- /dev/null
+++ b/Devices/m5stack-sticks3/device.properties
@@ -0,0 +1,25 @@
+[general]
+vendor=M5Stack
+name=StickS3
+
+[apps]
+launcherAppId=Launcher
+autoStartAppId=ApWebServer
+
+[hardware]
+target=ESP32S3
+flashSize=8MB
+spiRam=true
+spiRamMode=OCT
+spiRamSpeed=80M
+esptoolFlashFreq=80M
+tinyUsb=true
+
+[display]
+size=1.14"
+shape=rectangle
+dpi=242
+
+[lvgl]
+colorDepth=16
+uiDensity=compact
diff --git a/Devices/m5stack-sticks3/devicetree.yaml b/Devices/m5stack-sticks3/devicetree.yaml
new file mode 100644
index 000000000..1022f4de9
--- /dev/null
+++ b/Devices/m5stack-sticks3/devicetree.yaml
@@ -0,0 +1,5 @@
+dependencies:
+- Platforms/platform-esp32
+- Drivers/bmi270-module
+- Drivers/m5pm1-module
+dts: m5stack,sticks3.dts
diff --git a/Devices/m5stack-sticks3/m5stack,sticks3.dts b/Devices/m5stack-sticks3/m5stack,sticks3.dts
new file mode 100644
index 000000000..37ae19fa2
--- /dev/null
+++ b/Devices/m5stack-sticks3/m5stack,sticks3.dts
@@ -0,0 +1,71 @@
+/dts-v1/;
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+/ {
+ compatible = "root";
+ model = "M5Stack StickS3";
+
+ gpio0 {
+ compatible = "espressif,esp32-gpio";
+ gpio-count = <49>;
+ };
+
+ i2c_internal {
+ compatible = "espressif,esp32-i2c";
+ port = ;
+ clock-frequency = <100000>;
+ pin-sda = <&gpio0 47 GPIO_FLAG_NONE>;
+ pin-scl = <&gpio0 48 GPIO_FLAG_NONE>;
+
+ m5pm1 {
+ compatible = "m5stack,m5pm1";
+ reg = <0x6E>;
+ };
+
+ bmi270 {
+ compatible = "bosch,bmi270";
+ reg = <0x68>;
+ };
+ };
+
+ i2c_grove {
+ compatible = "espressif,esp32-i2c";
+ port = ;
+ clock-frequency = <400000>;
+ pin-sda = <&gpio0 9 GPIO_FLAG_NONE>;
+ pin-scl = <&gpio0 10 GPIO_FLAG_NONE>;
+ };
+
+ spi0 {
+ compatible = "espressif,esp32-spi";
+ host = ;
+ pin-mosi = <&gpio0 39 GPIO_FLAG_NONE>;
+ pin-sclk = <&gpio0 40 GPIO_FLAG_NONE>;
+ };
+
+ // Speaker and microphone (ES8311)
+ i2s0 {
+ compatible = "espressif,esp32-i2s";
+ port = ;
+ pin-bclk = <&gpio0 17 GPIO_FLAG_NONE>;
+ pin-ws = <&gpio0 15 GPIO_FLAG_NONE>;
+ pin-data-out = <&gpio0 14 GPIO_FLAG_NONE>;
+ pin-data-in = <&gpio0 16 GPIO_FLAG_NONE>;
+ pin-mclk = <&gpio0 18 GPIO_FLAG_NONE>;
+ };
+
+ uart_grove: uart1 {
+ compatible = "espressif,esp32-uart";
+ status = "disabled";
+ port = ;
+ pin-tx = <&gpio0 9 GPIO_FLAG_NONE>;
+ pin-rx = <&gpio0 10 GPIO_FLAG_NONE>;
+ };
+};
diff --git a/Devices/m5stack-tab5/CMakeLists.txt b/Devices/m5stack-tab5/CMakeLists.txt
index c84b1ada3..13e48e7e8 100644
--- a/Devices/m5stack-tab5/CMakeLists.txt
+++ b/Devices/m5stack-tab5/CMakeLists.txt
@@ -3,5 +3,5 @@ file(GLOB_RECURSE SOURCE_FILES Source/*.c*)
idf_component_register(
SRCS ${SOURCE_FILES}
INCLUDE_DIRS "Source"
- REQUIRES Tactility esp_lvgl_port esp_lcd EspLcdCompat esp_lcd_ili9881c GT911 PwmBacklight driver vfs fatfs
+ REQUIRES Tactility esp_lvgl_port esp_lcd EspLcdCompat esp_lcd_ili9881c esp_lcd_st7123 esp_lcd_touch_st7123 GT911 PwmBacklight driver vfs fatfs
)
diff --git a/Devices/m5stack-tab5/Source/Configuration.cpp b/Devices/m5stack-tab5/Source/Configuration.cpp
index 6a39ad0ce..cf8c79153 100644
--- a/Devices/m5stack-tab5/Source/Configuration.cpp
+++ b/Devices/m5stack-tab5/Source/Configuration.cpp
@@ -102,11 +102,17 @@ static void initExpander0(::Device* io_expander0) {
static void initExpander1(::Device* io_expander1) {
auto* c6_wlan_enable_pin = gpio_descriptor_acquire(io_expander1, GPIO_EXP1_PIN_C6_WLAN_ENABLE, GPIO_OWNER_GPIO);
+ check(c6_wlan_enable_pin);
auto* usb_a_5v_enable_pin = gpio_descriptor_acquire(io_expander1, GPIO_EXP1_PIN_USB_A_5V_ENABLE, GPIO_OWNER_GPIO);
+ check(usb_a_5v_enable_pin);
auto* device_power_pin = gpio_descriptor_acquire(io_expander1, GPIO_EXP1_PIN_DEVICE_POWER, GPIO_OWNER_GPIO);
+ check(device_power_pin);
auto* ip2326_ncharge_qc_enable_pin = gpio_descriptor_acquire(io_expander1, GPIO_EXP1_PIN_IP2326_NCHG_QC_EN, GPIO_OWNER_GPIO);
+ check(ip2326_ncharge_qc_enable_pin);
auto* ip2326_charge_state_led_pin = gpio_descriptor_acquire(io_expander1, GPIO_EXP1_PIN_IP2326_CHG_STAT_LED, GPIO_OWNER_GPIO);
+ check(ip2326_charge_state_led_pin);
auto* ip2326_charge_enable_pin = gpio_descriptor_acquire(io_expander1, GPIO_EXP1_PIN_IP2326_CHG_EN, GPIO_OWNER_GPIO);
+ check(ip2326_charge_enable_pin);
gpio_descriptor_set_flags(c6_wlan_enable_pin, GPIO_FLAG_DIRECTION_OUTPUT);
gpio_descriptor_set_flags(usb_a_5v_enable_pin, GPIO_FLAG_DIRECTION_OUTPUT);
diff --git a/Devices/m5stack-tab5/Source/devices/Detect.cpp b/Devices/m5stack-tab5/Source/devices/Detect.cpp
new file mode 100644
index 000000000..e70f2663b
--- /dev/null
+++ b/Devices/m5stack-tab5/Source/devices/Detect.cpp
@@ -0,0 +1,45 @@
+#include "Detect.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+static const auto LOGGER = tt::Logger("Tab5Detect");
+
+Tab5Variant detectVariant() {
+ // Allow time for touch IC to fully boot after expander reset in initBoot().
+ // 100ms is enough for I2C ACK (probe) but cold power-on needs ~300ms before
+ // register reads (read_fw_info) succeed reliably.
+ vTaskDelay(pdMS_TO_TICKS(300));
+
+ auto* i2c0 = device_find_by_name("i2c0");
+ check(i2c0);
+
+ constexpr auto PROBE_TIMEOUT = pdMS_TO_TICKS(50);
+
+ for (int attempt = 0; attempt < 3; ++attempt) {
+ // GT911 address depends on INT pin state during reset:
+ // GPIO 23 has a pull-up resistor to 3V3, so INT is high at reset → GT911 uses 0x5D (primary)
+ // It may also appear at 0x14 (backup) if the pin happened to be driven low
+ if (i2c_controller_has_device_at_address(i2c0, ESP_LCD_TOUCH_IO_I2C_GT911_ADDRESS, PROBE_TIMEOUT) == ERROR_NONE ||
+ i2c_controller_has_device_at_address(i2c0, ESP_LCD_TOUCH_IO_I2C_GT911_ADDRESS_BACKUP, PROBE_TIMEOUT) == ERROR_NONE) {
+ LOGGER.info("Detected GT911 touch — using ILI9881C display");
+ return Tab5Variant::Ili9881c_Gt911;
+ }
+
+ // Probe for ST7123 touch (new variant)
+ if (i2c_controller_has_device_at_address(i2c0, ESP_LCD_TOUCH_IO_I2C_ST7123_ADDRESS, PROBE_TIMEOUT) == ERROR_NONE) {
+ LOGGER.info("Detected ST7123 touch — using ST7123 display");
+ return Tab5Variant::St7123;
+ }
+
+ vTaskDelay(pdMS_TO_TICKS(100));
+ }
+
+ LOGGER.warn("No known touch controller detected, defaulting to ST7123");
+ return Tab5Variant::St7123;
+}
diff --git a/Devices/m5stack-tab5/Source/devices/Detect.h b/Devices/m5stack-tab5/Source/devices/Detect.h
new file mode 100644
index 000000000..96d159f45
--- /dev/null
+++ b/Devices/m5stack-tab5/Source/devices/Detect.h
@@ -0,0 +1,8 @@
+#pragma once
+
+enum class Tab5Variant {
+ Ili9881c_Gt911, // Older variant
+ St7123, // Newer variant (default)
+};
+
+[[nodiscard]] Tab5Variant detectVariant();
diff --git a/Devices/m5stack-tab5/Source/devices/Display.cpp b/Devices/m5stack-tab5/Source/devices/Display.cpp
index 3d74e1a04..b7516976f 100644
--- a/Devices/m5stack-tab5/Source/devices/Display.cpp
+++ b/Devices/m5stack-tab5/Source/devices/Display.cpp
@@ -1,16 +1,22 @@
+#include "Detect.h"
#include "Display.h"
#include "Ili9881cDisplay.h"
+#include "St7123Display.h"
+#include "St7123Touch.h"
#include
#include
#include
-#include
#include
+#include
+#include
+
+static const auto LOGGER = tt::Logger("Tab5Display");
constexpr auto LCD_PIN_RESET = GPIO_NUM_0; // Match P4 EV board reset line
constexpr auto LCD_PIN_BACKLIGHT = GPIO_NUM_22;
-static std::shared_ptr createTouch() {
+static std::shared_ptr createGt911Touch() {
auto configuration = std::make_unique(
I2C_NUM_0,
720,
@@ -19,25 +25,46 @@ static std::shared_ptr createTouch() {
false, // mirrorX
false, // mirrorY
GPIO_NUM_NC, // reset pin
- GPIO_NUM_NC // "GPIO_NUM_23 cannot be used due to resistor to 3V3" https://github.com/espressif/esp-bsp/blob/ad668c765cbad177495a122181df0a70ff9f8f61/bsp/m5stack_tab5/src/m5stack_tab5.c#L76234
+ GPIO_NUM_NC // "GPIO_NUM_23 cannot be used due to resistor to 3V3"
+ // https://github.com/espressif/esp-bsp/blob/ad668c765cbad177495a122181df0a70ff9f8f61/bsp/m5stack_tab5/src/m5stack_tab5.c#L76234
);
-
return std::make_shared(std::move(configuration));
}
+static std::shared_ptr createSt7123Touch() {
+ auto configuration = std::make_unique(
+ I2C_NUM_0,
+ 720,
+ 1280,
+ false, // swapXY
+ false, // mirrorX
+ false, // mirrorY
+ GPIO_NUM_23 // interrupt pin
+ );
+ return std::make_shared(std::move(configuration));
+}
+
std::shared_ptr createDisplay() {
// Initialize PWM backlight
if (!driver::pwmbacklight::init(LCD_PIN_BACKLIGHT, 5000, LEDC_TIMER_1, LEDC_CHANNEL_0)) {
- tt::Logger("Tab5").warn("Failed to initialize backlight");
+ LOGGER.warn("Failed to initialize backlight");
}
- auto touch = createTouch();
+ Tab5Variant variant = detectVariant();
+
+ std::shared_ptr touch;
- // Work-around to init touch : interrupt pin must be set to low
- // Note: There is a resistor to 3V3 on interrupt pin which is blocking GT911 touch
- // See https://github.com/espressif/esp-bsp/blob/ad668c765cbad177495a122181df0a70ff9f8f61/bsp/m5stack_tab5/src/m5stack_tab5.c#L777
- tt::hal::gpio::configure(23, tt::hal::gpio::Mode::Output, true, false);
- tt::hal::gpio::setLevel(23, false);
+ if (variant == Tab5Variant::St7123) {
+ touch = createSt7123Touch();
+ } else {
+ touch = createGt911Touch();
+
+ // Work-around to init GT911 touch: interrupt pin must be set to low
+ // Note: There is a resistor to 3V3 on interrupt pin which is blocking GT911 touch
+ // See https://github.com/espressif/esp-bsp/blob/ad668c765cbad177495a122181df0a70ff9f8f61/bsp/m5stack_tab5/src/m5stack_tab5.c#L777
+ tt::hal::gpio::configure(23, tt::hal::gpio::Mode::Output, true, false);
+ tt::hal::gpio::setLevel(23, false);
+ }
auto configuration = std::make_shared(EspLcdConfiguration {
.horizontalResolution = 720,
@@ -50,6 +77,8 @@ std::shared_ptr createDisplay() {
.mirrorY = false,
.invertColor = false,
.bufferSize = 0, // 0 = default (1/10 of screen)
+ .swRotate = (variant == Tab5Variant::St7123), // ST7123 MIPI-DSI panel does not support hardware swap_xy
+ .buffSpiram = (variant == Tab5Variant::St7123), // sw_rotate needs a 3rd buffer; use PSRAM to avoid OOM in internal SRAM
.touch = touch,
.backlightDutyFunction = driver::pwmbacklight::setBacklightDuty,
.resetPin = LCD_PIN_RESET,
@@ -59,6 +88,13 @@ std::shared_ptr createDisplay() {
.bitsPerPixel = 16
});
- const auto display = std::make_shared(configuration);
- return std::static_pointer_cast(display);
+ if (variant == Tab5Variant::St7123) {
+ return std::static_pointer_cast(
+ std::make_shared(configuration)
+ );
+ } else {
+ return std::static_pointer_cast(
+ std::make_shared(configuration)
+ );
+ }
}
diff --git a/Devices/m5stack-tab5/Source/devices/Ili9881cDisplay.cpp b/Devices/m5stack-tab5/Source/devices/Ili9881cDisplay.cpp
index b088d8842..0ba634267 100644
--- a/Devices/m5stack-tab5/Source/devices/Ili9881cDisplay.cpp
+++ b/Devices/m5stack-tab5/Source/devices/Ili9881cDisplay.cpp
@@ -1,5 +1,5 @@
#include "Ili9881cDisplay.h"
-#include "disp_init_data.h"
+#include "ili9881_init_data.h"
#include
#include
@@ -47,6 +47,8 @@ bool Ili9881cDisplay::createMipiDsiBus() {
if (esp_lcd_new_dsi_bus(&bus_config, &mipiDsiBus) != ESP_OK) {
LOGGER.error("Failed to create bus");
+ esp_ldo_release_channel(ldoChannel);
+ ldoChannel = nullptr;
return false;
}
@@ -67,6 +69,10 @@ bool Ili9881cDisplay::createIoHandle(esp_lcd_panel_io_handle_t& ioHandle) {
if (esp_lcd_new_panel_io_dbi(mipiDsiBus, &dbi_config, &ioHandle) != ESP_OK) {
LOGGER.error("Failed to create panel IO");
+ esp_lcd_del_dsi_bus(mipiDsiBus);
+ mipiDsiBus = nullptr;
+ esp_ldo_release_channel(ldoChannel);
+ ldoChannel = nullptr;
return false;
}
@@ -108,15 +114,15 @@ bool Ili9881cDisplay::createPanelHandle(esp_lcd_panel_io_handle_t ioHandle, cons
.vsync_back_porch = 20,
.vsync_front_porch = 20,
},
- .flags {
+ .flags = {
.use_dma2d = 1, // TODO: true?
.disable_lp = 0,
}
};
ili9881c_vendor_config_t vendor_config = {
- .init_cmds = disp_init_data,
- .init_cmds_size = std::size(disp_init_data),
+ .init_cmds = ili9881_init_data,
+ .init_cmds_size = std::size(ili9881_init_data),
.mipi_config = {
.dsi_bus = mipiDsiBus,
.dpi_config = &dpi_config,
diff --git a/Devices/m5stack-tab5/Source/devices/St7123Display.cpp b/Devices/m5stack-tab5/Source/devices/St7123Display.cpp
new file mode 100644
index 000000000..f1fd991eb
--- /dev/null
+++ b/Devices/m5stack-tab5/Source/devices/St7123Display.cpp
@@ -0,0 +1,148 @@
+#include "St7123Display.h"
+#include "st7123_init_data.h"
+
+#include
+#include
+
+static const auto LOGGER = tt::Logger("St7123");
+
+St7123Display::~St7123Display() {
+ // TODO: This should happen during ::stop(), but this isn't currently exposed
+ if (mipiDsiBus != nullptr) {
+ esp_lcd_del_dsi_bus(mipiDsiBus);
+ mipiDsiBus = nullptr;
+ }
+ if (ldoChannel != nullptr) {
+ esp_ldo_release_channel(ldoChannel);
+ ldoChannel = nullptr;
+ }
+}
+
+bool St7123Display::createMipiDsiBus() {
+ esp_ldo_channel_config_t ldo_mipi_phy_config = {
+ .chan_id = 3,
+ .voltage_mv = 2500,
+ .flags = {
+ .adjustable = 0,
+ .owned_by_hw = 0,
+ .bypass = 0
+ }
+ };
+
+ if (esp_ldo_acquire_channel(&ldo_mipi_phy_config, &ldoChannel) != ESP_OK) {
+ LOGGER.error("Failed to acquire LDO channel for MIPI DSI PHY");
+ return false;
+ }
+
+ LOGGER.info("Powered on");
+
+ const esp_lcd_dsi_bus_config_t bus_config = {
+ .bus_id = 0,
+ .num_data_lanes = 2,
+ .phy_clk_src = MIPI_DSI_PHY_CLK_SRC_DEFAULT,
+ .lane_bit_rate_mbps = 965 // ST7123 lane bitrate per M5Stack BSP
+ };
+
+ if (esp_lcd_new_dsi_bus(&bus_config, &mipiDsiBus) != ESP_OK) {
+ LOGGER.error("Failed to create bus");
+ esp_ldo_release_channel(ldoChannel);
+ ldoChannel = nullptr;
+ return false;
+ }
+
+ LOGGER.info("Bus created");
+ return true;
+}
+
+bool St7123Display::createIoHandle(esp_lcd_panel_io_handle_t& ioHandle) {
+ if (mipiDsiBus == nullptr) {
+ if (!createMipiDsiBus()) {
+ return false;
+ }
+ }
+
+ // DBI interface for LCD commands/parameters (8-bit cmd/param per ST7123 spec)
+ esp_lcd_dbi_io_config_t dbi_config = {
+ .virtual_channel = 0,
+ .lcd_cmd_bits = 8,
+ .lcd_param_bits = 8,
+ };
+
+ if (esp_lcd_new_panel_io_dbi(mipiDsiBus, &dbi_config, &ioHandle) != ESP_OK) {
+ LOGGER.error("Failed to create panel IO");
+ esp_lcd_del_dsi_bus(mipiDsiBus);
+ mipiDsiBus = nullptr;
+ esp_ldo_release_channel(ldoChannel);
+ ldoChannel = nullptr;
+ return false;
+ }
+
+ return true;
+}
+
+esp_lcd_panel_dev_config_t St7123Display::createPanelConfig(std::shared_ptr espLcdConfiguration, gpio_num_t resetPin) {
+ return {
+ .reset_gpio_num = resetPin,
+ .rgb_ele_order = espLcdConfiguration->rgbElementOrder,
+ .data_endian = LCD_RGB_DATA_ENDIAN_LITTLE,
+ .bits_per_pixel = 16,
+ .flags = {
+ .reset_active_high = 0
+ },
+ .vendor_config = nullptr
+ };
+}
+
+bool St7123Display::createPanelHandle(esp_lcd_panel_io_handle_t ioHandle, const esp_lcd_panel_dev_config_t& panelConfig, esp_lcd_panel_handle_t& panelHandle) {
+ esp_lcd_dpi_panel_config_t dpi_config = {
+ .virtual_channel = 0,
+ .dpi_clk_src = MIPI_DSI_DPI_CLK_SRC_DEFAULT,
+ .dpi_clock_freq_mhz = 70,
+ .pixel_format = LCD_COLOR_PIXEL_FORMAT_RGB565,
+ .num_fbs = 1,
+ .video_timing = {
+ .h_size = 720,
+ .v_size = 1280,
+ .hsync_pulse_width = 2,
+ .hsync_back_porch = 40,
+ .hsync_front_porch = 40,
+ .vsync_pulse_width = 2,
+ .vsync_back_porch = 8,
+ .vsync_front_porch = 220,
+ },
+ .flags = {
+ .use_dma2d = 1,
+ .disable_lp = 0,
+ }
+ };
+
+ st7123_vendor_config_t vendor_config = {
+ .init_cmds = st7123_init_data,
+ .init_cmds_size = std::size(st7123_init_data),
+ .mipi_config = {
+ .dsi_bus = mipiDsiBus,
+ .dpi_config = &dpi_config,
+ },
+ };
+
+ // Create a mutable copy of panelConfig to set vendor_config
+ esp_lcd_panel_dev_config_t mutable_panel_config = panelConfig;
+ mutable_panel_config.vendor_config = &vendor_config;
+
+ if (esp_lcd_new_panel_st7123(ioHandle, &mutable_panel_config, &panelHandle) != ESP_OK) {
+ LOGGER.error("Failed to create panel");
+ return false;
+ }
+
+ LOGGER.info("Panel created successfully");
+ return true;
+}
+
+lvgl_port_display_dsi_cfg_t St7123Display::getLvglPortDisplayDsiConfig(esp_lcd_panel_io_handle_t /*ioHandle*/, esp_lcd_panel_handle_t /*panelHandle*/) {
+ // Disable avoid_tearing to prevent stalls/blank flashes when other tasks (e.g. flash writes) block timing
+ return lvgl_port_display_dsi_cfg_t{
+ .flags = {
+ .avoid_tearing = 0,
+ },
+ };
+}
diff --git a/Devices/m5stack-tab5/Source/devices/St7123Display.h b/Devices/m5stack-tab5/Source/devices/St7123Display.h
new file mode 100644
index 000000000..adb9a28ab
--- /dev/null
+++ b/Devices/m5stack-tab5/Source/devices/St7123Display.h
@@ -0,0 +1,38 @@
+#pragma once
+
+#include
+
+#include
+#include
+
+class St7123Display final : public EspLcdDisplayV2 {
+
+ esp_lcd_dsi_bus_handle_t mipiDsiBus = nullptr;
+ esp_ldo_channel_handle_t ldoChannel = nullptr;
+
+ bool createMipiDsiBus();
+
+protected:
+
+ bool createIoHandle(esp_lcd_panel_io_handle_t& ioHandle) override;
+
+ esp_lcd_panel_dev_config_t createPanelConfig(std::shared_ptr espLcdConfiguration, gpio_num_t resetPin) override;
+
+ bool createPanelHandle(esp_lcd_panel_io_handle_t ioHandle, const esp_lcd_panel_dev_config_t& panelConfig, esp_lcd_panel_handle_t& panelHandle) override;
+
+ bool useDsiPanel() const override { return true; }
+
+ lvgl_port_display_dsi_cfg_t getLvglPortDisplayDsiConfig(esp_lcd_panel_io_handle_t /*ioHandle*/, esp_lcd_panel_handle_t /*panelHandle*/) override;
+
+public:
+
+ St7123Display(
+ const std::shared_ptr& configuration
+ ) : EspLcdDisplayV2(configuration) {}
+
+ ~St7123Display() override;
+
+ std::string getName() const override { return "St7123"; }
+
+ std::string getDescription() const override { return "St7123 MIPI-DSI display"; }
+};
diff --git a/Devices/m5stack-tab5/Source/devices/St7123Touch.cpp b/Devices/m5stack-tab5/Source/devices/St7123Touch.cpp
new file mode 100644
index 000000000..1b2aa0c92
--- /dev/null
+++ b/Devices/m5stack-tab5/Source/devices/St7123Touch.cpp
@@ -0,0 +1,42 @@
+#include "St7123Touch.h"
+
+#include
+#include
+#include
+
+static const auto LOGGER = tt::Logger("ST7123Touch");
+
+bool St7123Touch::createIoHandle(esp_lcd_panel_io_handle_t& outHandle) {
+ esp_lcd_panel_io_i2c_config_t io_config = ESP_LCD_TOUCH_IO_I2C_ST7123_CONFIG();
+ return esp_lcd_new_panel_io_i2c(
+ static_cast(configuration->port),
+ &io_config,
+ &outHandle
+ ) == ESP_OK;
+}
+
+bool St7123Touch::createTouchHandle(esp_lcd_panel_io_handle_t ioHandle, const esp_lcd_touch_config_t& config, esp_lcd_touch_handle_t& touchHandle) {
+ return esp_lcd_touch_new_i2c_st7123(ioHandle, &config, &touchHandle) == ESP_OK;
+}
+
+esp_lcd_touch_config_t St7123Touch::createEspLcdTouchConfig() {
+ return {
+ .x_max = configuration->xMax,
+ .y_max = configuration->yMax,
+ .rst_gpio_num = GPIO_NUM_NC,
+ .int_gpio_num = configuration->pinInterrupt,
+ .levels = {
+ .reset = 0,
+ .interrupt = 0,
+ },
+ .flags = {
+ .swap_xy = configuration->swapXy,
+ .mirror_x = configuration->mirrorX,
+ .mirror_y = configuration->mirrorY,
+ },
+ .process_coordinates = nullptr,
+ .interrupt_callback = nullptr,
+ .user_data = nullptr,
+ .driver_data = nullptr
+ };
+}
diff --git a/Devices/m5stack-tab5/Source/devices/St7123Touch.h b/Devices/m5stack-tab5/Source/devices/St7123Touch.h
new file mode 100644
index 000000000..2c3ddb4d4
--- /dev/null
+++ b/Devices/m5stack-tab5/Source/devices/St7123Touch.h
@@ -0,0 +1,59 @@
+#pragma once
+
+#include
+#include
+#include
+
+class St7123Touch final : public EspLcdTouch {
+
+public:
+
+ class Configuration {
+ public:
+
+ Configuration(
+ i2c_port_t port,
+ uint16_t xMax,
+ uint16_t yMax,
+ bool swapXy = false,
+ bool mirrorX = false,
+ bool mirrorY = false,
+ gpio_num_t pinInterrupt = GPIO_NUM_NC
+ ) : port(port),
+ xMax(xMax),
+ yMax(yMax),
+ swapXy(swapXy),
+ mirrorX(mirrorX),
+ mirrorY(mirrorY),
+ pinInterrupt(pinInterrupt)
+ {}
+
+ i2c_port_t port;
+ uint16_t xMax;
+ uint16_t yMax;
+ bool swapXy;
+ bool mirrorX;
+ bool mirrorY;
+ gpio_num_t pinInterrupt;
+ };
+
+private:
+
+ std::unique_ptr configuration;
+
+ bool createIoHandle(esp_lcd_panel_io_handle_t& outHandle) override;
+
+ bool createTouchHandle(esp_lcd_panel_io_handle_t ioHandle, const esp_lcd_touch_config_t& config, esp_lcd_touch_handle_t& touchHandle) override;
+
+ esp_lcd_touch_config_t createEspLcdTouchConfig() override;
+
+public:
+
+ explicit St7123Touch(std::unique_ptr inConfiguration) : configuration(std::move(inConfiguration)) {
+ assert(configuration != nullptr);
+ }
+
+ std::string getName() const override { return "ST7123Touch"; }
+
+ std::string getDescription() const override { return "ST7123 I2C touch driver"; }
+};
diff --git a/Devices/m5stack-tab5/Source/devices/disp_init_data.h b/Devices/m5stack-tab5/Source/devices/ili9881_init_data.h
similarity index 99%
rename from Devices/m5stack-tab5/Source/devices/disp_init_data.h
rename to Devices/m5stack-tab5/Source/devices/ili9881_init_data.h
index 2ddb42641..8e72baf08 100644
--- a/Devices/m5stack-tab5/Source/devices/disp_init_data.h
+++ b/Devices/m5stack-tab5/Source/devices/ili9881_init_data.h
@@ -6,7 +6,7 @@
#pragma once
#include
-static const ili9881c_lcd_init_cmd_t disp_init_data[] = {
+static const ili9881c_lcd_init_cmd_t ili9881_init_data[] = {
// {cmd, { data }, data_size, delay}
/**** CMD_Page 1 ****/
diff --git a/Devices/m5stack-tab5/Source/devices/st7123_init_data.h b/Devices/m5stack-tab5/Source/devices/st7123_init_data.h
new file mode 100644
index 000000000..90c16a39a
--- /dev/null
+++ b/Devices/m5stack-tab5/Source/devices/st7123_init_data.h
@@ -0,0 +1,39 @@
+/*
+ * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+#pragma once
+#include
+
+//Refer to https://github.com/m5stack/M5Tab5-UserDemo
+//https://github.com/m5stack/M5Tab5-UserDemo/blob/main/LICENSE
+static const st7123_lcd_init_cmd_t st7123_init_data[] = {
+ {0x60, (uint8_t[]){0x71, 0x23, 0xa2}, 3, 0},
+ {0x60, (uint8_t[]){0x71, 0x23, 0xa3}, 3, 0},
+ {0x60, (uint8_t[]){0x71, 0x23, 0xa4}, 3, 0},
+ {0xA4, (uint8_t[]){0x31}, 1, 0},
+ {0xD7, (uint8_t[]){0x10, 0x0A, 0x10, 0x2A, 0x80, 0x80}, 6, 0},
+ {0x90, (uint8_t[]){0x71, 0x23, 0x5A, 0x20, 0x24, 0x09, 0x09}, 7, 0},
+ {0xA3, (uint8_t[]){0x80, 0x01, 0x88, 0x30, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46, 0x00, 0x00, 0x1E, 0x5C, 0x1E, 0x80, 0x00, 0x4F, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46, 0x00, 0x00, 0x1E, 0x5C, 0x1E, 0x80, 0x00, 0x6F, 0x58, 0x00, 0x00, 0x00, 0xFF}, 40, 0},
+ {0xA6, (uint8_t[]){0x03, 0x00, 0x24, 0x55, 0x36, 0x00, 0x39, 0x00, 0x6E, 0x6E, 0x91, 0xFF, 0x00, 0x24, 0x55, 0x38, 0x00, 0x37, 0x00, 0x6E, 0x6E, 0x91, 0xFF, 0x00, 0x24, 0x11, 0x00, 0x00, 0x00, 0x00, 0x6E, 0x6E, 0x91, 0xFF, 0x00, 0xEC, 0x11, 0x00, 0x03, 0x00, 0x03, 0x6E, 0x6E, 0xFF, 0xFF, 0x00, 0x08, 0x80, 0x08, 0x80, 0x06, 0x00, 0x00, 0x00, 0x00}, 55, 0},
+ {0xA7, (uint8_t[]){0x19, 0x19, 0x80, 0x64, 0x40, 0x07, 0x16, 0x40, 0x00, 0x44, 0x03, 0x6E, 0x6E, 0x91, 0xFF, 0x08, 0x80, 0x64, 0x40, 0x25, 0x34, 0x40, 0x00, 0x02, 0x01, 0x6E, 0x6E, 0x91, 0xFF, 0x08, 0x80, 0x64, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x6E, 0x6E, 0x91, 0xFF, 0x08, 0x80, 0x64, 0x40, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x6E, 0x6E, 0x84, 0xFF, 0x08, 0x80, 0x44}, 60, 0},
+ {0xAC, (uint8_t[]){0x03, 0x19, 0x19, 0x18, 0x18, 0x06, 0x13, 0x13, 0x11, 0x11, 0x08, 0x08, 0x0A, 0x0A, 0x1C, 0x1C, 0x07, 0x07, 0x00, 0x00, 0x02, 0x02, 0x01, 0x19, 0x19, 0x18, 0x18, 0x06, 0x12, 0x12, 0x10, 0x10, 0x09, 0x09, 0x0B, 0x0B, 0x1C, 0x1C, 0x07, 0x07, 0x03, 0x03, 0x01, 0x01}, 44, 0},
+ {0xAD, (uint8_t[]){0xF0, 0x00, 0x46, 0x00, 0x03, 0x50, 0x50, 0xFF, 0xFF, 0xF0, 0x40, 0x06, 0x01, 0x07, 0x42, 0x42, 0xFF, 0xFF, 0x01, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF}, 25, 0},
+ {0xAE, (uint8_t[]){0xFE, 0x3F, 0x3F, 0xFE, 0x3F, 0x3F, 0x00}, 7, 0},
+ {0xB2, (uint8_t[]){0x15, 0x19, 0x05, 0x23, 0x49, 0xAF, 0x03, 0x2E, 0x5C, 0xD2, 0xFF, 0x10, 0x20, 0xFD, 0x20, 0xC0, 0x00}, 17, 0},
+ {0xE8, (uint8_t[]){0x20, 0x6F, 0x04, 0x97, 0x97, 0x3E, 0x04, 0xDC, 0xDC, 0x3E, 0x06, 0xFA, 0x26, 0x3E}, 15, 0},
+ {0x75, (uint8_t[]){0x03, 0x04}, 2, 0},
+ {0xE7, (uint8_t[]){0x3B, 0x00, 0x00, 0x7C, 0xA1, 0x8C, 0x20, 0x1A, 0xF0, 0xB1, 0x50, 0x00, 0x50, 0xB1, 0x50, 0xB1, 0x50, 0xD8, 0x00, 0x55, 0x00, 0xB1, 0x00, 0x45, 0xC9, 0x6A, 0xFF, 0x5A, 0xD8, 0x18, 0x88, 0x15, 0xB1, 0x01, 0x01, 0x77}, 36, 0},
+ {0xEA, (uint8_t[]){0x13, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x2C}, 8, 0},
+ {0xB0, (uint8_t[]){0x22, 0x43, 0x11, 0x61, 0x25, 0x43, 0x43}, 7, 0},
+ {0xB7, (uint8_t[]){0x00, 0x00, 0x73, 0x73}, 0x04, 0},
+ {0xBF, (uint8_t[]){0xA6, 0xAA}, 2, 0},
+ {0xA9, (uint8_t[]){0x00, 0x00, 0x73, 0xFF, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03}, 10, 0},
+ {0xC8, (uint8_t[]){0x00, 0x00, 0x10, 0x1F, 0x36, 0x00, 0x5D, 0x04, 0x9D, 0x05, 0x10, 0xF2, 0x06, 0x60, 0x03, 0x11, 0xAD, 0x00, 0xEF, 0x01, 0x22, 0x2E, 0x0E, 0x74, 0x08, 0x32, 0xDC, 0x09, 0x33, 0x0F, 0xF3, 0x77, 0x0D, 0xB0, 0xDC, 0x03, 0xFF}, 37, 0},
+ {0xC9, (uint8_t[]){0x00, 0x00, 0x10, 0x1F, 0x36, 0x00, 0x5D, 0x04, 0x9D, 0x05, 0x10, 0xF2, 0x06, 0x60, 0x03, 0x11, 0xAD, 0x00, 0xEF, 0x01, 0x22, 0x2E, 0x0E, 0x74, 0x08, 0x32, 0xDC, 0x09, 0x33, 0x0F, 0xF3, 0x77, 0x0D, 0xB0, 0xDC, 0x03, 0xFF}, 37, 0},
+ {0x36, (uint8_t[]){0x00}, 1, 0},
+ {0x11, (uint8_t[]){0x00}, 1, 100},
+ {0x29, (uint8_t[]){0x00}, 1, 0},
+ {0x35, (uint8_t[]){0x00}, 1, 100},
+};
diff --git a/Devices/m5stack-tab5/devicetree.yaml b/Devices/m5stack-tab5/devicetree.yaml
index bc6b7b156..d6cb798ab 100644
--- a/Devices/m5stack-tab5/devicetree.yaml
+++ b/Devices/m5stack-tab5/devicetree.yaml
@@ -1,5 +1,6 @@
dependencies:
- - Platforms/platform-esp32
- - Drivers/pi4ioe5v6408-module
- - Drivers/bmi270-module
+- Platforms/platform-esp32
+- Drivers/pi4ioe5v6408-module
+- Drivers/bmi270-module
+- Drivers/rx8130ce-module
dts: m5stack,tab5.dts
diff --git a/Devices/m5stack-tab5/m5stack,tab5.dts b/Devices/m5stack-tab5/m5stack,tab5.dts
index 620001d98..93fc843dc 100644
--- a/Devices/m5stack-tab5/m5stack,tab5.dts
+++ b/Devices/m5stack-tab5/m5stack,tab5.dts
@@ -7,6 +7,7 @@
#include
#include
#include
+#include
/ {
compatible = "root";
@@ -20,7 +21,7 @@
i2c_internal: i2c0 {
compatible = "espressif,esp32-i2c";
port = ;
- clock-frequency = <400000>;
+ clock-frequency = <100000>;
pin-sda = <&gpio0 31 GPIO_FLAG_NONE>;
pin-scl = <&gpio0 32 GPIO_FLAG_NONE>;
@@ -38,6 +39,11 @@
compatible = "bosch,bmi270";
reg = <0x68>;
};
+
+ rx8130ce {
+ compatible = "epson,rx8130ce";
+ reg = <0x32>;
+ };
};
i2c_port_a: i2c1 {
diff --git a/Devices/waveshare-s3-lcd-13/devicetree.yaml b/Devices/waveshare-s3-lcd-13/devicetree.yaml
index 9321802ee..1b96c2dd5 100644
--- a/Devices/waveshare-s3-lcd-13/devicetree.yaml
+++ b/Devices/waveshare-s3-lcd-13/devicetree.yaml
@@ -1,3 +1,4 @@
dependencies:
- - Platforms/platform-esp32
+- Platforms/platform-esp32
+- Drivers/qmi8658-module
dts: waveshare,s3-lcd-13.dts
diff --git a/Devices/waveshare-s3-lcd-13/waveshare,s3-lcd-13.dts b/Devices/waveshare-s3-lcd-13/waveshare,s3-lcd-13.dts
index 97d815782..0acee4dfe 100644
--- a/Devices/waveshare-s3-lcd-13/waveshare,s3-lcd-13.dts
+++ b/Devices/waveshare-s3-lcd-13/waveshare,s3-lcd-13.dts
@@ -4,6 +4,7 @@
#include
#include
#include
+#include
// Reference: https://www.waveshare.com/wiki/ESP32-S3-LCD-1.3
/ {
@@ -21,6 +22,11 @@
clock-frequency = <400000>;
pin-sda = <&gpio0 47 GPIO_FLAG_NONE>;
pin-scl = <&gpio0 48 GPIO_FLAG_NONE>;
+
+ qmi8658 {
+ compatible = "qst,qmi8658";
+ reg = <0x6B>;
+ };
};
spi0 {
diff --git a/Devices/waveshare-s3-touch-lcd-128/devicetree.yaml b/Devices/waveshare-s3-touch-lcd-128/devicetree.yaml
index e51ea4a19..117dac1af 100644
--- a/Devices/waveshare-s3-touch-lcd-128/devicetree.yaml
+++ b/Devices/waveshare-s3-touch-lcd-128/devicetree.yaml
@@ -1,3 +1,4 @@
dependencies:
- - Platforms/platform-esp32
+- Platforms/platform-esp32
+- Drivers/qmi8658-module
dts: waveshare,s3-touch-lcd-128.dts
diff --git a/Devices/waveshare-s3-touch-lcd-128/waveshare,s3-touch-lcd-128.dts b/Devices/waveshare-s3-touch-lcd-128/waveshare,s3-touch-lcd-128.dts
index d5f3c955a..5a9ca28a7 100644
--- a/Devices/waveshare-s3-touch-lcd-128/waveshare,s3-touch-lcd-128.dts
+++ b/Devices/waveshare-s3-touch-lcd-128/waveshare,s3-touch-lcd-128.dts
@@ -4,6 +4,7 @@
#include
#include
#include
+#include
// Reference: https://www.waveshare.com/wiki/ESP32-S3-Touch-LCD-1.28
/ {
@@ -21,6 +22,11 @@
clock-frequency = <400000>;
pin-sda = <&gpio0 6 GPIO_FLAG_NONE>;
pin-scl = <&gpio0 7 GPIO_FLAG_NONE>;
+
+ qmi8658 {
+ compatible = "qst,qmi8658";
+ reg = <0x6B>;
+ };
};
display_spi: spi0 {
diff --git a/Documentation/ideas.md b/Documentation/ideas.md
index 625d4388f..664221e09 100644
--- a/Documentation/ideas.md
+++ b/Documentation/ideas.md
@@ -29,7 +29,6 @@
- Fix glitches when installing app via App Hub with 4.3" Waveshare
- TCA9534 keyboards should use interrupts
- GT911 drivers should use interrupts if it's stable
-- Change ButtonControl to work with interrupts and xQueue
- Fix Cardputer (original): use LV_KEY_NEXT and _PREV in keyboard mapping instead of encoder driver hack (and check GPIO app if it then hangs too)
- Logging with a function that uses std::format
- Expose http::download() and main dispatcher to TactiltyC.
diff --git a/Drivers/ButtonControl/Source/ButtonControl.cpp b/Drivers/ButtonControl/Source/ButtonControl.cpp
index e7607b5b3..d9f4b558f 100644
--- a/Drivers/ButtonControl/Source/ButtonControl.cpp
+++ b/Drivers/ButtonControl/Source/ButtonControl.cpp
@@ -1,22 +1,52 @@
#include "ButtonControl.h"
+#include
#include
#include
static const auto LOGGER = tt::Logger("ButtonControl");
-ButtonControl::ButtonControl(const std::vector& pinConfigurations) : pinConfigurations(pinConfigurations) {
+ButtonControl::ButtonControl(const std::vector& pinConfigurations)
+ : buttonQueue(20, sizeof(ButtonEvent)),
+ pinConfigurations(pinConfigurations) {
+
pinStates.resize(pinConfigurations.size());
- for (const auto& pinConfiguration : pinConfigurations) {
- tt::hal::gpio::configure(pinConfiguration.pin, tt::hal::gpio::Mode::Input, false, false);
+
+ // Build isrArgs with one entry per unique physical pin, then configure GPIO.
+ isrArgs.reserve(pinConfigurations.size());
+ for (size_t i = 0; i < pinConfigurations.size(); i++) {
+ const auto pin = static_cast(pinConfigurations[i].pin);
+
+ // Skip if this physical pin was already seen.
+ bool seen = false;
+ for (const auto& arg : isrArgs) {
+ if (arg.pin == pin) { seen = true; break; }
+ }
+ if (seen) continue;
+
+ gpio_config_t io_conf = {
+ .pin_bit_mask = 1ULL << pin,
+ .mode = GPIO_MODE_INPUT,
+ .pull_up_en = GPIO_PULLUP_DISABLE,
+ .pull_down_en = GPIO_PULLDOWN_DISABLE,
+ .intr_type = GPIO_INTR_ANYEDGE,
+ };
+ esp_err_t err = gpio_config(&io_conf);
+ if (err != ESP_OK) {
+ LOGGER.error("Failed to configure GPIO {}: {}", static_cast(pin), esp_err_to_name(err));
+ continue;
+ }
+
+ // isrArgs is reserved upfront; push_back will not reallocate, keeping addresses stable
+ // for gpio_isr_handler_add() called later in startThread().
+ isrArgs.push_back({ .self = this, .pin = pin });
}
}
ButtonControl::~ButtonControl() {
if (driverThread != nullptr && driverThread->getState() != tt::Thread::State::Stopped) {
- interruptDriverThread = true;
- driverThread->join();
+ stopThread();
}
}
@@ -48,7 +78,7 @@ void ButtonControl::readCallback(lv_indev_t* indev, lv_indev_data_t* data) {
data->state = LV_INDEV_STATE_PRESSED;
break;
case Action::AppClose:
- // TODO: implement
+ tt::app::stop();
break;
}
}
@@ -57,57 +87,86 @@ void ButtonControl::readCallback(lv_indev_t* indev, lv_indev_data_t* data) {
}
}
-void ButtonControl::updatePin(std::vector::const_reference configuration, std::vector::reference state) {
- if (tt::hal::gpio::getLevel(configuration.pin)) { // if pressed
- if (state.pressState) {
- // check time for long press trigger
- auto time_passed = tt::kernel::getMillis() - state.pressStartTime;
- if (time_passed > 500) {
- // state.triggerLongPress = true;
- }
- } else {
- state.pressStartTime = tt::kernel::getMillis();
- state.pressState = true;
- }
+void ButtonControl::updatePin(std::vector::const_reference configuration, std::vector::reference state, bool pressed) {
+ auto now = tt::kernel::getMillis();
+
+ // Software debounce: ignore edges within 20ms of the last state change.
+ if ((now - state.lastChangeTime) < 20) {
+ return;
+ }
+ state.lastChangeTime = now;
+
+ if (pressed) {
+ state.pressStartTime = now;
+ state.pressState = true;
} else { // released
if (state.pressState) {
- auto time_passed = tt::kernel::getMillis() - state.pressStartTime;
+ auto time_passed = now - state.pressStartTime;
if (time_passed < 500) {
- LOGGER.debug("Trigger short press");
+ LOGGER.info("Short press ({}ms)", time_passed);
state.triggerShortPress = true;
+ } else {
+ LOGGER.info("Long press ({}ms)", time_passed);
+ state.triggerLongPress = true;
}
state.pressState = false;
}
}
}
+void IRAM_ATTR ButtonControl::gpioIsrHandler(void* arg) {
+ auto* isrArg = static_cast(arg);
+ ButtonEvent event {
+ .pin = isrArg->pin,
+ .pressed = gpio_get_level(isrArg->pin) == 0, // active-low: LOW = pressed
+ };
+ // tt::MessageQueue::put() is ISR-safe with timeout=0: it detects ISR context via
+ // xPortInIsrContext() and uses xQueueSendFromISR() + portYIELD_FROM_ISR() internally.
+ isrArg->self->buttonQueue.put(&event, 0);
+}
+
void ButtonControl::driverThreadMain() {
- while (!shouldInterruptDriverThread()) {
- if (mutex.lock(100)) {
- for (int i = 0; i < pinConfigurations.size(); i++) {
- updatePin(pinConfigurations[i], pinStates[i]);
+ ButtonEvent event;
+ while (buttonQueue.get(&event, portMAX_DELAY)) {
+ if (event.pin == GPIO_NUM_NC) {
+ break; // shutdown sentinel
+ }
+ LOGGER.info("Pin {} {}", static_cast(event.pin), event.pressed ? "down" : "up");
+ if (mutex.lock(portMAX_DELAY)) {
+ // Update ALL PinConfiguration entries that share this physical pin.
+ for (size_t i = 0; i < pinConfigurations.size(); i++) {
+ if (static_cast(pinConfigurations[i].pin) == event.pin) {
+ updatePin(pinConfigurations[i], pinStates[i], event.pressed);
+ }
}
mutex.unlock();
}
- tt::kernel::delayMillis(5);
}
}
-bool ButtonControl::shouldInterruptDriverThread() const {
- bool interrupt = false;
- if (mutex.lock(50 / portTICK_PERIOD_MS)) {
- interrupt = interruptDriverThread;
- mutex.unlock();
- }
- return interrupt;
-}
-
-void ButtonControl::startThread() {
+bool ButtonControl::startThread() {
LOGGER.info("Start");
- mutex.lock();
+ esp_err_t err = gpio_install_isr_service(ESP_INTR_FLAG_IRAM);
+ if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
+ LOGGER.error("Failed to install GPIO ISR service: {}", esp_err_to_name(err));
+ return false;
+ }
- interruptDriverThread = false;
+ // isrArgs has one entry per unique physical pin — no duplicate registrations.
+ // Addresses are stable: vector was reserved in constructor and is not modified after that.
+ int handlersAdded = 0;
+ for (auto& arg : isrArgs) {
+ err = gpio_isr_handler_add(arg.pin, gpioIsrHandler, &arg);
+ if (err != ESP_OK) {
+ LOGGER.error("Failed to add ISR for GPIO {}: {}", static_cast(arg.pin), esp_err_to_name(err));
+ for (int i = 0; i < handlersAdded; i++) {
+ gpio_isr_handler_remove(isrArgs[i].pin);
+ }
+ return false;
+ }
+ handlersAdded++;
+ }
driverThread = std::make_shared("ButtonControl", 4096, [this] {
driverThreadMain();
@@ -115,22 +174,21 @@ void ButtonControl::startThread() {
});
driverThread->start();
-
- mutex.unlock();
+ return true;
}
void ButtonControl::stopThread() {
LOGGER.info("Stop");
- mutex.lock();
- interruptDriverThread = true;
- mutex.unlock();
+ for (const auto& arg : isrArgs) {
+ gpio_isr_handler_remove(arg.pin);
+ }
- driverThread->join();
+ ButtonEvent sentinel { .pin = GPIO_NUM_NC, .pressed = false };
+ buttonQueue.put(&sentinel, portMAX_DELAY);
- mutex.lock();
+ driverThread->join();
driverThread = nullptr;
- mutex.unlock();
}
bool ButtonControl::startLvgl(lv_display_t* display) {
@@ -138,7 +196,9 @@ bool ButtonControl::startLvgl(lv_display_t* display) {
return false;
}
- startThread();
+ if (!startThread()) {
+ return false;
+ }
deviceHandle = lv_indev_create();
lv_indev_set_type(deviceHandle, LV_INDEV_TYPE_ENCODER);
diff --git a/Drivers/ButtonControl/Source/ButtonControl.h b/Drivers/ButtonControl/Source/ButtonControl.h
index 146830904..5aab931b9 100644
--- a/Drivers/ButtonControl/Source/ButtonControl.h
+++ b/Drivers/ButtonControl/Source/ButtonControl.h
@@ -2,9 +2,12 @@
#include
#include
+#include
#include
#include
+#include
+
class ButtonControl final : public tt::hal::encoder::EncoderDevice {
public:
@@ -31,28 +34,41 @@ class ButtonControl final : public tt::hal::encoder::EncoderDevice {
struct PinState {
long pressStartTime = 0;
- long pressReleaseTime = 0;
+ long lastChangeTime = 0;
bool pressState = false;
bool triggerShortPress = false;
bool triggerLongPress = false;
};
+ /** Queued from ISR to worker thread. pin == GPIO_NUM_NC is a shutdown sentinel. */
+ struct ButtonEvent {
+ gpio_num_t pin;
+ bool pressed;
+ };
+
+ /** One entry per unique physical pin; addresses must remain stable after construction. */
+ struct IsrArg {
+ ButtonControl* self;
+ gpio_num_t pin;
+ };
+
lv_indev_t* deviceHandle = nullptr;
std::shared_ptr driverThread;
- bool interruptDriverThread = false;
tt::Mutex mutex;
+ tt::MessageQueue buttonQueue;
std::vector pinConfigurations;
std::vector pinStates;
+ std::vector isrArgs; // one entry per unique physical pin
- bool shouldInterruptDriverThread() const;
-
- static void updatePin(std::vector::const_reference value, std::vector::reference pin_state);
+ static void updatePin(std::vector::const_reference config, std::vector::reference state, bool pressed);
void driverThreadMain();
static void readCallback(lv_indev_t* indev, lv_indev_data_t* data);
- void startThread();
+ static void IRAM_ATTR gpioIsrHandler(void* arg);
+
+ bool startThread();
void stopThread();
public:
diff --git a/Drivers/EspLcdCompat/Source/EspLcdDisplayV2.cpp b/Drivers/EspLcdCompat/Source/EspLcdDisplayV2.cpp
index 27061170f..6559939c8 100644
--- a/Drivers/EspLcdCompat/Source/EspLcdDisplayV2.cpp
+++ b/Drivers/EspLcdCompat/Source/EspLcdDisplayV2.cpp
@@ -180,9 +180,9 @@ lvgl_port_display_cfg_t EspLcdDisplayV2::getLvglPortDisplayConfig(std::shared_pt
},
.color_format = configuration->lvglColorFormat,
.flags = {
- .buff_dma = 1,
- .buff_spiram = 0,
- .sw_rotate = 0,
+ .buff_dma = configuration->buffSpiram ? 0u : 1u,
+ .buff_spiram = configuration->buffSpiram ? 1u : 0u,
+ .sw_rotate = configuration->swRotate ? 1u : 0u,
.swap_bytes = configuration->lvglSwapBytes,
.full_refresh = 0,
.direct_mode = 0
diff --git a/Drivers/EspLcdCompat/Source/EspLcdDisplayV2.h b/Drivers/EspLcdCompat/Source/EspLcdDisplayV2.h
index 5d843c4bd..9f919171c 100644
--- a/Drivers/EspLcdCompat/Source/EspLcdDisplayV2.h
+++ b/Drivers/EspLcdCompat/Source/EspLcdDisplayV2.h
@@ -20,6 +20,8 @@ struct EspLcdConfiguration {
bool mirrorY;
bool invertColor;
uint32_t bufferSize; // Size in pixel count. 0 means default, which is 1/10 of the screen size
+ bool swRotate = false; // Use LVGL software rotation instead of hardware swap_xy (required for MIPI-DSI panels that don't support swap_xy)
+ bool buffSpiram = false; // Allocate LVGL draw buffers from PSRAM instead of DMA-capable internal SRAM (required when sw_rotate needs a 3rd buffer that won't fit in internal SRAM)
std::shared_ptr touch;
std::function _Nullable backlightDutyFunction;
gpio_num_t resetPin;
diff --git a/Drivers/bm8563-module/CMakeLists.txt b/Drivers/bm8563-module/CMakeLists.txt
new file mode 100644
index 000000000..4c4f20b18
--- /dev/null
+++ b/Drivers/bm8563-module/CMakeLists.txt
@@ -0,0 +1,11 @@
+cmake_minimum_required(VERSION 3.20)
+
+include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake")
+
+file(GLOB_RECURSE SOURCE_FILES "source/*.c*")
+
+tactility_add_module(bm8563-module
+ SRCS ${SOURCE_FILES}
+ INCLUDE_DIRS include/
+ REQUIRES TactilityKernel
+)
diff --git a/Drivers/bm8563-module/LICENSE-Apache-2.0.md b/Drivers/bm8563-module/LICENSE-Apache-2.0.md
new file mode 100644
index 000000000..f5f4b8b5e
--- /dev/null
+++ b/Drivers/bm8563-module/LICENSE-Apache-2.0.md
@@ -0,0 +1,195 @@
+Apache License
+==============
+
+_Version 2.0, January 2004_
+_<>_
+
+### Terms and Conditions for use, reproduction, and distribution
+
+#### 1. Definitions
+
+“License” shall mean the terms and conditions for use, reproduction, and
+distribution as defined by Sections 1 through 9 of this document.
+
+“Licensor” shall mean the copyright owner or entity authorized by the copyright
+owner that is granting the License.
+
+“Legal Entity” shall mean the union of the acting entity and all other entities
+that control, are controlled by, or are under common control with that entity.
+For the purposes of this definition, “control” means **(i)** the power, direct or
+indirect, to cause the direction or management of such entity, whether by
+contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
+outstanding shares, or **(iii)** beneficial ownership of such entity.
+
+“You” (or “Your”) shall mean an individual or Legal Entity exercising
+permissions granted by this License.
+
+“Source” form shall mean the preferred form for making modifications, including
+but not limited to software source code, documentation source, and configuration
+files.
+
+“Object” form shall mean any form resulting from mechanical transformation or
+translation of a Source form, including but not limited to compiled object code,
+generated documentation, and conversions to other media types.
+
+“Work” shall mean the work of authorship, whether in Source or Object form, made
+available under the License, as indicated by a copyright notice that is included
+in or attached to the work (an example is provided in the Appendix below).
+
+“Derivative Works” shall mean any work, whether in Source or Object form, that
+is based on (or derived from) the Work and for which the editorial revisions,
+annotations, elaborations, or other modifications represent, as a whole, an
+original work of authorship. For the purposes of this License, Derivative Works
+shall not include works that remain separable from, or merely link (or bind by
+name) to the interfaces of, the Work and Derivative Works thereof.
+
+“Contribution” shall mean any work of authorship, including the original version
+of the Work and any modifications or additions to that Work or Derivative Works
+thereof, that is intentionally submitted to Licensor for inclusion in the Work
+by the copyright owner or by an individual or Legal Entity authorized to submit
+on behalf of the copyright owner. For the purposes of this definition,
+“submitted” means any form of electronic, verbal, or written communication sent
+to the Licensor or its representatives, including but not limited to
+communication on electronic mailing lists, source code control systems, and
+issue tracking systems that are managed by, or on behalf of, the Licensor for
+the purpose of discussing and improving the Work, but excluding communication
+that is conspicuously marked or otherwise designated in writing by the copyright
+owner as “Not a Contribution.”
+
+“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
+of whom a Contribution has been received by Licensor and subsequently
+incorporated within the Work.
+
+#### 2. Grant of Copyright License
+
+Subject to the terms and conditions of this License, each Contributor hereby
+grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
+irrevocable copyright license to reproduce, prepare Derivative Works of,
+publicly display, publicly perform, sublicense, and distribute the Work and such
+Derivative Works in Source or Object form.
+
+#### 3. Grant of Patent License
+
+Subject to the terms and conditions of this License, each Contributor hereby
+grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
+irrevocable (except as stated in this section) patent license to make, have
+made, use, offer to sell, sell, import, and otherwise transfer the Work, where
+such license applies only to those patent claims licensable by such Contributor
+that are necessarily infringed by their Contribution(s) alone or by combination
+of their Contribution(s) with the Work to which such Contribution(s) was
+submitted. If You institute patent litigation against any entity (including a
+cross-claim or counterclaim in a lawsuit) alleging that the Work or a
+Contribution incorporated within the Work constitutes direct or contributory
+patent infringement, then any patent licenses granted to You under this License
+for that Work shall terminate as of the date such litigation is filed.
+
+#### 4. Redistribution
+
+You may reproduce and distribute copies of the Work or Derivative Works thereof
+in any medium, with or without modifications, and in Source or Object form,
+provided that You meet the following conditions:
+
+* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
+this License; and
+* **(b)** You must cause any modified files to carry prominent notices stating that You
+changed the files; and
+* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
+all copyright, patent, trademark, and attribution notices from the Source form
+of the Work, excluding those notices that do not pertain to any part of the
+Derivative Works; and
+* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
+Derivative Works that You distribute must include a readable copy of the
+attribution notices contained within such NOTICE file, excluding those notices
+that do not pertain to any part of the Derivative Works, in at least one of the
+following places: within a NOTICE text file distributed as part of the
+Derivative Works; within the Source form or documentation, if provided along
+with the Derivative Works; or, within a display generated by the Derivative
+Works, if and wherever such third-party notices normally appear. The contents of
+the NOTICE file are for informational purposes only and do not modify the
+License. You may add Your own attribution notices within Derivative Works that
+You distribute, alongside or as an addendum to the NOTICE text from the Work,
+provided that such additional attribution notices cannot be construed as
+modifying the License.
+
+You may add Your own copyright statement to Your modifications and may provide
+additional or different license terms and conditions for use, reproduction, or
+distribution of Your modifications, or for any such Derivative Works as a whole,
+provided Your use, reproduction, and distribution of the Work otherwise complies
+with the conditions stated in this License.
+
+#### 5. Submission of Contributions
+
+Unless You explicitly state otherwise, any Contribution intentionally submitted
+for inclusion in the Work by You to the Licensor shall be under the terms and
+conditions of this License, without any additional terms or conditions.
+Notwithstanding the above, nothing herein shall supersede or modify the terms of
+any separate license agreement you may have executed with Licensor regarding
+such Contributions.
+
+#### 6. Trademarks
+
+This License does not grant permission to use the trade names, trademarks,
+service marks, or product names of the Licensor, except as required for
+reasonable and customary use in describing the origin of the Work and
+reproducing the content of the NOTICE file.
+
+#### 7. Disclaimer of Warranty
+
+Unless required by applicable law or agreed to in writing, Licensor provides the
+Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
+including, without limitation, any warranties or conditions of TITLE,
+NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
+solely responsible for determining the appropriateness of using or
+redistributing the Work and assume any risks associated with Your exercise of
+permissions under this License.
+
+#### 8. Limitation of Liability
+
+In no event and under no legal theory, whether in tort (including negligence),
+contract, or otherwise, unless required by applicable law (such as deliberate
+and grossly negligent acts) or agreed to in writing, shall any Contributor be
+liable to You for damages, including any direct, indirect, special, incidental,
+or consequential damages of any character arising as a result of this License or
+out of the use or inability to use the Work (including but not limited to
+damages for loss of goodwill, work stoppage, computer failure or malfunction, or
+any and all other commercial damages or losses), even if such Contributor has
+been advised of the possibility of such damages.
+
+#### 9. Accepting Warranty or Additional Liability
+
+While redistributing the Work or Derivative Works thereof, You may choose to
+offer, and charge a fee for, acceptance of support, warranty, indemnity, or
+other liability obligations and/or rights consistent with this License. However,
+in accepting such obligations, You may act only on Your own behalf and on Your
+sole responsibility, not on behalf of any other Contributor, and only if You
+agree to indemnify, defend, and hold each Contributor harmless for any liability
+incurred by, or claims asserted against, such Contributor by reason of your
+accepting any such warranty or additional liability.
+
+_END OF TERMS AND CONDITIONS_
+
+### APPENDIX: How to apply the Apache License to your work
+
+To apply the Apache License to your work, attach the following boilerplate
+notice, with the fields enclosed by brackets `[]` replaced with your own
+identifying information. (Don't include the brackets!) The text should be
+enclosed in the appropriate comment syntax for the file format. We also
+recommend that a file or class name and description of purpose be included on
+the same “printed page” as the copyright notice for easier identification within
+third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
diff --git a/Drivers/bm8563-module/README.md b/Drivers/bm8563-module/README.md
new file mode 100644
index 000000000..80e9816fe
--- /dev/null
+++ b/Drivers/bm8563-module/README.md
@@ -0,0 +1,9 @@
+# BM8563 I2C Driver
+
+A driver for the `BM8563` Realtime Clock from BELLING [SHANGHAI BELLING CO., LTD.]
+Drop-in functional clone — same init, same registers, same I2C protocol as the NXP PCF8563.
+
+See https://www.nxp.com/docs/en/data-sheet/PCF8563.pdf
+And: https://www.alldatasheet.com/datasheet-pdf/pdf/1768247/BELLING/BM8563.html
+
+License: [Apache v2.0](LICENSE-Apache-2.0.md)
diff --git a/Drivers/bm8563-module/bindings/belling,bm8563.yaml b/Drivers/bm8563-module/bindings/belling,bm8563.yaml
new file mode 100644
index 000000000..c9aea5ce7
--- /dev/null
+++ b/Drivers/bm8563-module/bindings/belling,bm8563.yaml
@@ -0,0 +1,5 @@
+description: BM8563 RTC (PCF8563-compatible)
+
+include: [ "i2c-device.yaml" ]
+
+compatible: "belling,bm8563"
diff --git a/Drivers/bm8563-module/devicetree.yaml b/Drivers/bm8563-module/devicetree.yaml
new file mode 100644
index 000000000..99f3dfd7e
--- /dev/null
+++ b/Drivers/bm8563-module/devicetree.yaml
@@ -0,0 +1,3 @@
+dependencies:
+ - TactilityKernel
+bindings: bindings
\ No newline at end of file
diff --git a/Drivers/bm8563-module/include/bindings/bm8563.h b/Drivers/bm8563-module/include/bindings/bm8563.h
new file mode 100644
index 000000000..bebbf677a
--- /dev/null
+++ b/Drivers/bm8563-module/include/bindings/bm8563.h
@@ -0,0 +1,15 @@
+// SPDX-License-Identifier: Apache-2.0
+#pragma once
+
+#include
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+DEFINE_DEVICETREE(bm8563, struct Bm8563Config)
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/Drivers/bm8563-module/include/bm8563_module.h b/Drivers/bm8563-module/include/bm8563_module.h
new file mode 100644
index 000000000..c7d174056
--- /dev/null
+++ b/Drivers/bm8563-module/include/bm8563_module.h
@@ -0,0 +1,14 @@
+// SPDX-License-Identifier: Apache-2.0
+#pragma once
+
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+extern struct Module bm8563_module;
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/Drivers/bm8563-module/include/drivers/bm8563.h b/Drivers/bm8563-module/include/drivers/bm8563.h
new file mode 100644
index 000000000..dc5b57748
--- /dev/null
+++ b/Drivers/bm8563-module/include/drivers/bm8563.h
@@ -0,0 +1,45 @@
+// SPDX-License-Identifier: Apache-2.0
+#pragma once
+
+#include
+#include
+
+struct Device;
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct Bm8563Config {
+ /** Address on bus */
+ uint8_t address;
+};
+
+struct Bm8563DateTime {
+ uint16_t year; // 2000–2199
+ uint8_t month; // 1–12
+ uint8_t day; // 1–31
+ uint8_t hour; // 0–23
+ uint8_t minute; // 0–59
+ uint8_t second; // 0–59
+};
+
+/**
+ * Read the current date and time from the RTC.
+ * @param[in] device bm8563 device
+ * @param[out] dt Pointer to Bm8563DateTime to populate
+ * @return ERROR_NONE on success
+ */
+error_t bm8563_get_datetime(struct Device* device, struct Bm8563DateTime* dt);
+
+/**
+ * Write the date and time to the RTC.
+ * @param[in] device bm8563 device
+ * @param[in] dt Pointer to Bm8563DateTime to write (year must be 2000–2199)
+ * @return ERROR_NONE on success, ERROR_INVALID_ARGUMENT if any field is out of range
+ */
+error_t bm8563_set_datetime(struct Device* device, const struct Bm8563DateTime* dt);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/Drivers/bm8563-module/source/bm8563.cpp b/Drivers/bm8563-module/source/bm8563.cpp
new file mode 100644
index 000000000..4d3f7804e
--- /dev/null
+++ b/Drivers/bm8563-module/source/bm8563.cpp
@@ -0,0 +1,118 @@
+// SPDX-License-Identifier: Apache-2.0
+#include
+#include
+#include
+#include
+#include
+
+#define TAG "BM8563"
+
+static constexpr uint8_t REG_CTRL1 = 0x00; // Control status 1
+static constexpr uint8_t REG_SECONDS = 0x02; // Seconds BCD, bit 7 = VL (clock integrity)
+// Registers 0x02–0x08: seconds, minutes, hours, days, weekdays, months, years
+
+static constexpr TickType_t I2C_TIMEOUT_TICKS = pdMS_TO_TICKS(50);
+
+#define GET_CONFIG(device) (static_cast((device)->config))
+
+// region Helpers
+
+static uint8_t bcd_to_dec(uint8_t bcd) { return static_cast((bcd >> 4) * 10 + (bcd & 0x0F)); }
+static uint8_t dec_to_bcd(uint8_t dec) { return static_cast(((dec / 10) << 4) | (dec % 10)); }
+
+// endregion
+
+// region Driver lifecycle
+
+static error_t start(Device* device) {
+ auto* i2c_controller = device_get_parent(device);
+ if (device_get_type(i2c_controller) != &I2C_CONTROLLER_TYPE) {
+ LOG_E(TAG, "Parent is not an I2C controller");
+ return ERROR_RESOURCE;
+ }
+
+ auto address = GET_CONFIG(device)->address;
+
+ // Clear STOP bit — chip may have been stopped after a power cycle
+ if (i2c_controller_register8_set(i2c_controller, address, REG_CTRL1, 0x00, I2C_TIMEOUT_TICKS) != ERROR_NONE) {
+ LOG_E(TAG, "Failed to clear STOP bit at 0x%02X", address);
+ return ERROR_RESOURCE;
+ }
+
+ return ERROR_NONE;
+}
+
+static error_t stop(Device* device) {
+ // RTC oscillator should continue running on battery backup
+ // No action needed on driver stop
+ return ERROR_NONE;
+}
+
+// endregion
+
+extern "C" {
+
+error_t bm8563_get_datetime(Device* device, Bm8563DateTime* dt) {
+ auto* i2c_controller = device_get_parent(device);
+ auto address = GET_CONFIG(device)->address;
+
+ // Burst-read 7 registers starting at 0x02:
+ // [0]=seconds [1]=minutes [2]=hours [3]=days [4]=weekdays [5]=months [6]=years
+ uint8_t buf[7] = {};
+ error_t error = i2c_controller_read_register(i2c_controller, address, REG_SECONDS, buf, sizeof(buf), I2C_TIMEOUT_TICKS);
+ if (error != ERROR_NONE) return error;
+
+ if (buf[0] & 0x80u) {
+ LOG_E(TAG, "Clock integrity compromised (VL flag set) — data unreliable");
+ return ERROR_INVALID_STATE;
+ }
+ dt->second = bcd_to_dec(buf[0] & 0x7Fu); // mask VL flag
+ dt->minute = bcd_to_dec(buf[1] & 0x7Fu);
+ dt->hour = bcd_to_dec(buf[2] & 0x3Fu);
+ dt->day = bcd_to_dec(buf[3] & 0x3Fu);
+ // buf[4] = weekday — ignored
+ dt->month = bcd_to_dec(buf[5] & 0x1Fu);
+ bool century = (buf[5] & 0x80u) != 0;
+ dt->year = static_cast(2000 + bcd_to_dec(buf[6]) + (century ? 100 : 0));
+
+ return ERROR_NONE;
+}
+
+error_t bm8563_set_datetime(Device* device, const Bm8563DateTime* dt) {
+ if (dt->year < 2000 || dt->year > 2199 ||
+ dt->month < 1 || dt->month > 12 ||
+ dt->day < 1 || dt->day > 31 ||
+ dt->hour > 23 || dt->minute > 59 || dt->second > 59) {
+ return ERROR_INVALID_ARGUMENT;
+ }
+
+ auto* i2c_controller = device_get_parent(device);
+ auto address = GET_CONFIG(device)->address;
+
+ bool century = (dt->year >= 2100);
+ uint8_t y = static_cast(century ? dt->year - 2100 : dt->year - 2000);
+
+ uint8_t buf[7] = {};
+ buf[0] = dec_to_bcd(dt->second);
+ buf[1] = dec_to_bcd(dt->minute);
+ buf[2] = dec_to_bcd(dt->hour);
+ buf[3] = dec_to_bcd(dt->day);
+ buf[4] = 0; // weekday — leave as Sunday (unused)
+ buf[5] = static_cast(dec_to_bcd(dt->month) | (century ? 0x80u : 0x00u));
+ buf[6] = dec_to_bcd(y);
+
+ return i2c_controller_write_register(i2c_controller, address, REG_SECONDS, buf, sizeof(buf), I2C_TIMEOUT_TICKS);
+}
+
+Driver bm8563_driver = {
+ .name = "bm8563",
+ .compatible = (const char*[]) { "belling,bm8563", nullptr },
+ .start_device = start,
+ .stop_device = stop,
+ .api = nullptr,
+ .device_type = nullptr,
+ .owner = &bm8563_module,
+ .internal = nullptr
+};
+
+} // extern "C"
diff --git a/Drivers/bm8563-module/source/module.cpp b/Drivers/bm8563-module/source/module.cpp
new file mode 100644
index 000000000..259e029da
--- /dev/null
+++ b/Drivers/bm8563-module/source/module.cpp
@@ -0,0 +1,34 @@
+// SPDX-License-Identifier: Apache-2.0
+#include
+#include
+#include
+
+extern "C" {
+
+extern Driver bm8563_driver;
+
+static error_t start() {
+ /* We crash when construct fails, because if a single driver fails to construct,
+ * there is no guarantee that the previously constructed drivers can be destroyed */
+ check(driver_construct_add(&bm8563_driver) == ERROR_NONE);
+ return ERROR_NONE;
+}
+
+static error_t stop() {
+ /* We crash when destruct fails, because if a single driver fails to destruct,
+ * there is no guarantee that the previously destroyed drivers can be recovered */
+ check(driver_remove_destruct(&bm8563_driver) == ERROR_NONE);
+ return ERROR_NONE;
+}
+
+extern const ModuleSymbol bm8563_module_symbols[];
+
+Module bm8563_module = {
+ .name = "bm8563",
+ .start = start,
+ .stop = stop,
+ .symbols = bm8563_module_symbols,
+ .internal = nullptr
+};
+
+}
diff --git a/Drivers/bm8563-module/source/symbols.c b/Drivers/bm8563-module/source/symbols.c
new file mode 100644
index 000000000..43312b41b
--- /dev/null
+++ b/Drivers/bm8563-module/source/symbols.c
@@ -0,0 +1,9 @@
+// SPDX-License-Identifier: Apache-2.0
+#include
+#include
+
+const struct ModuleSymbol bm8563_module_symbols[] = {
+ DEFINE_MODULE_SYMBOL(bm8563_get_datetime),
+ DEFINE_MODULE_SYMBOL(bm8563_set_datetime),
+ MODULE_SYMBOL_TERMINATOR
+};
diff --git a/Drivers/bmi270-module/source/bmi270.cpp b/Drivers/bmi270-module/source/bmi270.cpp
index fbc6b487b..d4ce55807 100644
--- a/Drivers/bmi270-module/source/bmi270.cpp
+++ b/Drivers/bmi270-module/source/bmi270.cpp
@@ -131,6 +131,20 @@ static error_t start(Device* device) {
}
static error_t stop(Device* device) {
+ auto* i2c_controller = device_get_parent(device);
+ if (device_get_type(i2c_controller) != &I2C_CONTROLLER_TYPE) {
+ LOG_E(TAG, "Parent is not an I2C controller");
+ return ERROR_RESOURCE;
+ }
+
+ auto address = GET_CONFIG(device)->address;
+
+ // Disable accelerometer and gyroscope (clear bit1=gyr_en, bit2=acc_en)
+ if (i2c_controller_register8_set(i2c_controller, address, REG_PWR_CTRL, 0x00, I2C_TIMEOUT_TICKS) != ERROR_NONE) {
+ LOG_E(TAG, "Failed to put BMI270 to sleep");
+ return ERROR_RESOURCE;
+ }
+
return ERROR_NONE;
}
diff --git a/Drivers/m5pm1-module/CMakeLists.txt b/Drivers/m5pm1-module/CMakeLists.txt
new file mode 100644
index 000000000..1dfa2ac68
--- /dev/null
+++ b/Drivers/m5pm1-module/CMakeLists.txt
@@ -0,0 +1,11 @@
+cmake_minimum_required(VERSION 3.20)
+
+include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake")
+
+file(GLOB_RECURSE SOURCE_FILES "source/*.c*")
+
+tactility_add_module(m5pm1-module
+ SRCS ${SOURCE_FILES}
+ INCLUDE_DIRS include/
+ REQUIRES TactilityKernel
+)
diff --git a/Drivers/m5pm1-module/LICENSE-Apache-2.0.md b/Drivers/m5pm1-module/LICENSE-Apache-2.0.md
new file mode 100644
index 000000000..f5f4b8b5e
--- /dev/null
+++ b/Drivers/m5pm1-module/LICENSE-Apache-2.0.md
@@ -0,0 +1,195 @@
+Apache License
+==============
+
+_Version 2.0, January 2004_
+_<>_
+
+### Terms and Conditions for use, reproduction, and distribution
+
+#### 1. Definitions
+
+“License” shall mean the terms and conditions for use, reproduction, and
+distribution as defined by Sections 1 through 9 of this document.
+
+“Licensor” shall mean the copyright owner or entity authorized by the copyright
+owner that is granting the License.
+
+“Legal Entity” shall mean the union of the acting entity and all other entities
+that control, are controlled by, or are under common control with that entity.
+For the purposes of this definition, “control” means **(i)** the power, direct or
+indirect, to cause the direction or management of such entity, whether by
+contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
+outstanding shares, or **(iii)** beneficial ownership of such entity.
+
+“You” (or “Your”) shall mean an individual or Legal Entity exercising
+permissions granted by this License.
+
+“Source” form shall mean the preferred form for making modifications, including
+but not limited to software source code, documentation source, and configuration
+files.
+
+“Object” form shall mean any form resulting from mechanical transformation or
+translation of a Source form, including but not limited to compiled object code,
+generated documentation, and conversions to other media types.
+
+“Work” shall mean the work of authorship, whether in Source or Object form, made
+available under the License, as indicated by a copyright notice that is included
+in or attached to the work (an example is provided in the Appendix below).
+
+“Derivative Works” shall mean any work, whether in Source or Object form, that
+is based on (or derived from) the Work and for which the editorial revisions,
+annotations, elaborations, or other modifications represent, as a whole, an
+original work of authorship. For the purposes of this License, Derivative Works
+shall not include works that remain separable from, or merely link (or bind by
+name) to the interfaces of, the Work and Derivative Works thereof.
+
+“Contribution” shall mean any work of authorship, including the original version
+of the Work and any modifications or additions to that Work or Derivative Works
+thereof, that is intentionally submitted to Licensor for inclusion in the Work
+by the copyright owner or by an individual or Legal Entity authorized to submit
+on behalf of the copyright owner. For the purposes of this definition,
+“submitted” means any form of electronic, verbal, or written communication sent
+to the Licensor or its representatives, including but not limited to
+communication on electronic mailing lists, source code control systems, and
+issue tracking systems that are managed by, or on behalf of, the Licensor for
+the purpose of discussing and improving the Work, but excluding communication
+that is conspicuously marked or otherwise designated in writing by the copyright
+owner as “Not a Contribution.”
+
+“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
+of whom a Contribution has been received by Licensor and subsequently
+incorporated within the Work.
+
+#### 2. Grant of Copyright License
+
+Subject to the terms and conditions of this License, each Contributor hereby
+grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
+irrevocable copyright license to reproduce, prepare Derivative Works of,
+publicly display, publicly perform, sublicense, and distribute the Work and such
+Derivative Works in Source or Object form.
+
+#### 3. Grant of Patent License
+
+Subject to the terms and conditions of this License, each Contributor hereby
+grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
+irrevocable (except as stated in this section) patent license to make, have
+made, use, offer to sell, sell, import, and otherwise transfer the Work, where
+such license applies only to those patent claims licensable by such Contributor
+that are necessarily infringed by their Contribution(s) alone or by combination
+of their Contribution(s) with the Work to which such Contribution(s) was
+submitted. If You institute patent litigation against any entity (including a
+cross-claim or counterclaim in a lawsuit) alleging that the Work or a
+Contribution incorporated within the Work constitutes direct or contributory
+patent infringement, then any patent licenses granted to You under this License
+for that Work shall terminate as of the date such litigation is filed.
+
+#### 4. Redistribution
+
+You may reproduce and distribute copies of the Work or Derivative Works thereof
+in any medium, with or without modifications, and in Source or Object form,
+provided that You meet the following conditions:
+
+* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
+this License; and
+* **(b)** You must cause any modified files to carry prominent notices stating that You
+changed the files; and
+* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
+all copyright, patent, trademark, and attribution notices from the Source form
+of the Work, excluding those notices that do not pertain to any part of the
+Derivative Works; and
+* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
+Derivative Works that You distribute must include a readable copy of the
+attribution notices contained within such NOTICE file, excluding those notices
+that do not pertain to any part of the Derivative Works, in at least one of the
+following places: within a NOTICE text file distributed as part of the
+Derivative Works; within the Source form or documentation, if provided along
+with the Derivative Works; or, within a display generated by the Derivative
+Works, if and wherever such third-party notices normally appear. The contents of
+the NOTICE file are for informational purposes only and do not modify the
+License. You may add Your own attribution notices within Derivative Works that
+You distribute, alongside or as an addendum to the NOTICE text from the Work,
+provided that such additional attribution notices cannot be construed as
+modifying the License.
+
+You may add Your own copyright statement to Your modifications and may provide
+additional or different license terms and conditions for use, reproduction, or
+distribution of Your modifications, or for any such Derivative Works as a whole,
+provided Your use, reproduction, and distribution of the Work otherwise complies
+with the conditions stated in this License.
+
+#### 5. Submission of Contributions
+
+Unless You explicitly state otherwise, any Contribution intentionally submitted
+for inclusion in the Work by You to the Licensor shall be under the terms and
+conditions of this License, without any additional terms or conditions.
+Notwithstanding the above, nothing herein shall supersede or modify the terms of
+any separate license agreement you may have executed with Licensor regarding
+such Contributions.
+
+#### 6. Trademarks
+
+This License does not grant permission to use the trade names, trademarks,
+service marks, or product names of the Licensor, except as required for
+reasonable and customary use in describing the origin of the Work and
+reproducing the content of the NOTICE file.
+
+#### 7. Disclaimer of Warranty
+
+Unless required by applicable law or agreed to in writing, Licensor provides the
+Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
+including, without limitation, any warranties or conditions of TITLE,
+NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
+solely responsible for determining the appropriateness of using or
+redistributing the Work and assume any risks associated with Your exercise of
+permissions under this License.
+
+#### 8. Limitation of Liability
+
+In no event and under no legal theory, whether in tort (including negligence),
+contract, or otherwise, unless required by applicable law (such as deliberate
+and grossly negligent acts) or agreed to in writing, shall any Contributor be
+liable to You for damages, including any direct, indirect, special, incidental,
+or consequential damages of any character arising as a result of this License or
+out of the use or inability to use the Work (including but not limited to
+damages for loss of goodwill, work stoppage, computer failure or malfunction, or
+any and all other commercial damages or losses), even if such Contributor has
+been advised of the possibility of such damages.
+
+#### 9. Accepting Warranty or Additional Liability
+
+While redistributing the Work or Derivative Works thereof, You may choose to
+offer, and charge a fee for, acceptance of support, warranty, indemnity, or
+other liability obligations and/or rights consistent with this License. However,
+in accepting such obligations, You may act only on Your own behalf and on Your
+sole responsibility, not on behalf of any other Contributor, and only if You
+agree to indemnify, defend, and hold each Contributor harmless for any liability
+incurred by, or claims asserted against, such Contributor by reason of your
+accepting any such warranty or additional liability.
+
+_END OF TERMS AND CONDITIONS_
+
+### APPENDIX: How to apply the Apache License to your work
+
+To apply the Apache License to your work, attach the following boilerplate
+notice, with the fields enclosed by brackets `[]` replaced with your own
+identifying information. (Don't include the brackets!) The text should be
+enclosed in the appropriate comment syntax for the file format. We also
+recommend that a file or class name and description of purpose be included on
+the same “printed page” as the copyright notice for easier identification within
+third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
diff --git a/Drivers/m5pm1-module/README.md b/Drivers/m5pm1-module/README.md
new file mode 100644
index 000000000..b1e5fece1
--- /dev/null
+++ b/Drivers/m5pm1-module/README.md
@@ -0,0 +1,8 @@
+# M5PM1 I2C Driver
+
+A driver for the `M5PM1` power management chip.
+
+See https://m5stack-doc.oss-cn-shenzhen.aliyuncs.com/1207/M5PM1_Datasheet_EN.pdf
+And https://github.com/m5stack/M5PM1
+
+License: [Apache v2.0](LICENSE-Apache-2.0.md)
diff --git a/Drivers/m5pm1-module/bindings/m5stack,m5pm1.yaml b/Drivers/m5pm1-module/bindings/m5stack,m5pm1.yaml
new file mode 100644
index 000000000..b8957ec4d
--- /dev/null
+++ b/Drivers/m5pm1-module/bindings/m5stack,m5pm1.yaml
@@ -0,0 +1,5 @@
+description: M5Stack M5PM1 Power Management IC
+
+include: ["i2c-device.yaml"]
+
+compatible: "m5stack,m5pm1"
diff --git a/Drivers/m5pm1-module/devicetree.yaml b/Drivers/m5pm1-module/devicetree.yaml
new file mode 100644
index 000000000..a07d6f334
--- /dev/null
+++ b/Drivers/m5pm1-module/devicetree.yaml
@@ -0,0 +1,3 @@
+dependencies:
+ - TactilityKernel
+bindings: bindings
diff --git a/Drivers/m5pm1-module/include/bindings/m5pm1.h b/Drivers/m5pm1-module/include/bindings/m5pm1.h
new file mode 100644
index 000000000..a63d58bca
--- /dev/null
+++ b/Drivers/m5pm1-module/include/bindings/m5pm1.h
@@ -0,0 +1,15 @@
+// SPDX-License-Identifier: Apache-2.0
+#pragma once
+
+#include
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+DEFINE_DEVICETREE(m5pm1, struct M5pm1Config)
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/Drivers/m5pm1-module/include/drivers/m5pm1.h b/Drivers/m5pm1-module/include/drivers/m5pm1.h
new file mode 100644
index 000000000..32c6a2752
--- /dev/null
+++ b/Drivers/m5pm1-module/include/drivers/m5pm1.h
@@ -0,0 +1,88 @@
+// SPDX-License-Identifier: Apache-2.0
+#pragma once
+
+#include
+#include
+#include
+
+struct Device;
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct M5pm1Config {
+ uint8_t address;
+};
+
+// ---------------------------------------------------------------------------
+// Power source (REG_PWR_SRC 0x04)
+// ---------------------------------------------------------------------------
+typedef enum {
+ M5PM1_PWR_SRC_5VIN = 0,
+ M5PM1_PWR_SRC_5VINOUT = 1,
+ M5PM1_PWR_SRC_BAT = 2,
+ M5PM1_PWR_SRC_UNKNOWN = 3,
+} M5pm1PowerSource;
+
+// ---------------------------------------------------------------------------
+// Voltage readings
+// ---------------------------------------------------------------------------
+error_t m5pm1_get_battery_voltage(struct Device* device, uint16_t* mv);
+error_t m5pm1_get_vin_voltage(struct Device* device, uint16_t* mv);
+error_t m5pm1_get_5vout_voltage(struct Device* device, uint16_t* mv);
+error_t m5pm1_get_power_source(struct Device* device, M5pm1PowerSource* source);
+
+// ---------------------------------------------------------------------------
+// Charging & power rails
+// ---------------------------------------------------------------------------
+/** PM1_G0 low = charging (connected to charge-status pin of the charge IC) */
+error_t m5pm1_is_charging(struct Device* device, bool* charging);
+error_t m5pm1_set_charge_enable(struct Device* device, bool enable);
+error_t m5pm1_set_boost_enable(struct Device* device, bool enable); ///< 5V BOOST / Grove power
+error_t m5pm1_set_ldo_enable(struct Device* device, bool enable); ///< 3.3V LDO
+
+// ---------------------------------------------------------------------------
+// Temperature (internal chip sensor)
+// ---------------------------------------------------------------------------
+/** Returns temperature in units of 0.1 °C */
+error_t m5pm1_get_temperature(struct Device* device, uint16_t* decidegc);
+
+// ---------------------------------------------------------------------------
+// System commands
+// ---------------------------------------------------------------------------
+error_t m5pm1_shutdown(struct Device* device);
+error_t m5pm1_reboot(struct Device* device);
+
+// ---------------------------------------------------------------------------
+// Power button (M5PM1 internal button, not ESP32 GPIO)
+// ---------------------------------------------------------------------------
+/** Current instantaneous state of the power button */
+error_t m5pm1_btn_get_state(struct Device* device, bool* pressed);
+/** Edge-triggered flag — auto-clears on read */
+error_t m5pm1_btn_get_flag(struct Device* device, bool* was_pressed);
+
+// ---------------------------------------------------------------------------
+// Watchdog timer
+// ---------------------------------------------------------------------------
+/** timeout_sec: 0 = disabled, 1–255 = timeout in seconds */
+error_t m5pm1_wdt_set(struct Device* device, uint8_t timeout_sec);
+error_t m5pm1_wdt_feed(struct Device* device);
+
+// ---------------------------------------------------------------------------
+// RTC RAM (32 bytes, retained across sleep / power-off)
+// ---------------------------------------------------------------------------
+error_t m5pm1_read_rtc_ram(struct Device* device, uint8_t offset, uint8_t* data, uint8_t len);
+error_t m5pm1_write_rtc_ram(struct Device* device, uint8_t offset, const uint8_t* data, uint8_t len);
+
+// ---------------------------------------------------------------------------
+// NeoPixel LED (via M5PM1 LED controller, max 32 LEDs)
+// ---------------------------------------------------------------------------
+error_t m5pm1_set_led_count(struct Device* device, uint8_t count);
+error_t m5pm1_set_led_color(struct Device* device, uint8_t index, uint8_t r, uint8_t g, uint8_t b);
+error_t m5pm1_refresh_leds(struct Device* device);
+error_t m5pm1_disable_leds(struct Device* device);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/Drivers/m5pm1-module/include/m5pm1_module.h b/Drivers/m5pm1-module/include/m5pm1_module.h
new file mode 100644
index 000000000..9e5f3e73e
--- /dev/null
+++ b/Drivers/m5pm1-module/include/m5pm1_module.h
@@ -0,0 +1,14 @@
+// SPDX-License-Identifier: Apache-2.0
+#pragma once
+
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+extern struct Module m5pm1_module;
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/Drivers/m5pm1-module/source/m5pm1.cpp b/Drivers/m5pm1-module/source/m5pm1.cpp
new file mode 100644
index 000000000..c972eb051
--- /dev/null
+++ b/Drivers/m5pm1-module/source/m5pm1.cpp
@@ -0,0 +1,288 @@
+// SPDX-License-Identifier: Apache-2.0
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#define TAG "M5PM1"
+
+// ---------------------------------------------------------------------------
+// Register map
+// ---------------------------------------------------------------------------
+static constexpr uint8_t REG_DEVICE_ID = 0x00; ///< R - Device ID (0x50)
+static constexpr uint8_t REG_PWR_SRC = 0x04; ///< R - Power source (0=5VIN, 1=5VINOUT, 2=BAT)
+static constexpr uint8_t REG_PWR_CFG = 0x06; ///< RW - [3]=BOOST_EN [2]=LDO_EN [1]=DCDC_EN [0]=CHG_EN
+static constexpr uint8_t REG_I2C_CFG = 0x09; ///< RW - [4]=SPD(400kHz) [3:0]=SLP_TO(0=off)
+static constexpr uint8_t REG_WDT_CNT = 0x0A; ///< RW - Watchdog countdown (0=disabled, 1–255=seconds)
+static constexpr uint8_t REG_WDT_KEY = 0x0B; ///< W - Write 0xA5 to feed watchdog
+static constexpr uint8_t REG_SYS_CMD = 0x0C; ///< W - High nibble=0xA; low: 1=shutdown 2=reboot 3=download
+static constexpr uint8_t REG_GPIO_MODE = 0x10; ///< RW - GPIO direction [4:0] (1=output, 0=input)
+static constexpr uint8_t REG_GPIO_OUT = 0x11; ///< RW - GPIO output level [4:0]
+static constexpr uint8_t REG_GPIO_IN = 0x12; ///< R - GPIO input state [4:0]
+static constexpr uint8_t REG_GPIO_DRV = 0x13; ///< RW - Drive mode [4:0] (0=push-pull, 1=open-drain)
+static constexpr uint8_t REG_GPIO_FUNC0 = 0x16; ///< RW - GPIO0–3 function (2 bits each: 00=GPIO)
+static constexpr uint8_t REG_VBAT_L = 0x22; ///< R - Battery voltage low byte (mV, 16-bit LE)
+static constexpr uint8_t REG_VIN_L = 0x24; ///< R - VIN voltage low byte (mV, 16-bit LE)
+static constexpr uint8_t REG_5VOUT_L = 0x26; ///< R - 5V output voltage low byte (mV, 16-bit LE)
+static constexpr uint8_t REG_ADC_RES_L = 0x28; ///< R - ADC result low byte (mV, 16-bit LE)
+static constexpr uint8_t REG_ADC_CTRL = 0x2A; ///< RW - [3:1]=channel [0]=START
+static constexpr uint8_t REG_BTN_STATUS = 0x48; ///< R - [7]=BTN_FLAG(auto-clear) [0]=BTN_STATE
+static constexpr uint8_t REG_NEO_CFG = 0x50; ///< RW - [6]=REFRESH [5:0]=LED_CNT
+static constexpr uint8_t REG_NEO_DATA = 0x60; ///< RW - NeoPixel RGB565 data, 2 bytes per LED (max 32)
+static constexpr uint8_t REG_RTC_RAM = 0xA0; ///< RW - 32 bytes of RTC RAM
+
+// PWR_CFG bit masks
+static constexpr uint8_t PWR_CFG_CHG_EN = (1U << 0U);
+static constexpr uint8_t PWR_CFG_DCDC_EN = (1U << 1U);
+static constexpr uint8_t PWR_CFG_LDO_EN = (1U << 2U);
+static constexpr uint8_t PWR_CFG_BOOST_EN = (1U << 3U);
+
+// System command values (high nibble must be 0xA)
+static constexpr uint8_t SYS_CMD_SHUTDOWN = 0xA1;
+static constexpr uint8_t SYS_CMD_REBOOT = 0xA2;
+
+// ADC channel for temperature
+static constexpr uint8_t ADC_CH_TEMP = 6;
+
+// PM1_G2: LCD power enable on M5Stack StickS3
+static constexpr uint8_t LCD_POWER_BIT = (1U << 2U);
+
+static constexpr TickType_t TIMEOUT = pdMS_TO_TICKS(50);
+
+#define GET_CONFIG(device) (static_cast((device)->config))
+
+// ---------------------------------------------------------------------------
+// Driver lifecycle
+// ---------------------------------------------------------------------------
+
+static error_t start(Device* device) {
+ Device* i2c = device_get_parent(device);
+ if (device_get_type(i2c) != &I2C_CONTROLLER_TYPE) {
+ LOG_E(TAG, "Parent is not an I2C controller");
+ return ERROR_RESOURCE;
+ }
+
+ const uint8_t addr = GET_CONFIG(device)->address;
+
+ // M5PM1 enters I2C sleep after inactivity. The first transaction after sleep
+ // is ignored as the chip wakes up. Retry with increasing delays until ACK.
+ bool awake = false;
+ for (int attempt = 0; attempt < 5; attempt++) {
+ uint8_t chip_id = 0;
+ if (i2c_controller_register8_get(i2c, addr, REG_DEVICE_ID, &chip_id, TIMEOUT) == ERROR_NONE) {
+ LOG_I(TAG, "M5PM1 online (chip_id=0x%02X)", chip_id);
+ awake = true;
+ break;
+ }
+ vTaskDelay(pdMS_TO_TICKS(20 * (attempt + 1)));
+ }
+
+ if (!awake) {
+ LOG_E(TAG, "M5PM1 not responding — LCD power will not be enabled");
+ return ERROR_NONE; // non-fatal: don't crash the kernel
+ }
+
+ // Disable I2C idle sleep so the PMIC stays reachable on battery power
+ if (i2c_controller_register8_set(i2c, addr, REG_I2C_CFG, 0x00, TIMEOUT) != ERROR_NONE) {
+ LOG_W(TAG, "Failed to disable I2C sleep (non-fatal)");
+ }
+
+ // PM1_G2 → LCD power enable (L3B rail on StickS3)
+ // Sequence matches M5GFX: clear FUNC0 bit2, set MODE bit2 output, clear DRV bit2 push-pull, set OUT bit2 high
+ bool lcd_ok =
+ i2c_controller_register8_reset_bits(i2c, addr, REG_GPIO_FUNC0, LCD_POWER_BIT, TIMEOUT) == ERROR_NONE &&
+ i2c_controller_register8_set_bits (i2c, addr, REG_GPIO_MODE, LCD_POWER_BIT, TIMEOUT) == ERROR_NONE &&
+ i2c_controller_register8_reset_bits(i2c, addr, REG_GPIO_DRV, LCD_POWER_BIT, TIMEOUT) == ERROR_NONE &&
+ i2c_controller_register8_set_bits (i2c, addr, REG_GPIO_OUT, LCD_POWER_BIT, TIMEOUT) == ERROR_NONE;
+
+ if (lcd_ok) {
+ LOG_I(TAG, "LCD power enabled via PM1_G2");
+ } else {
+ LOG_E(TAG, "Failed to enable LCD power via PM1_G2");
+ }
+
+ return ERROR_NONE;
+}
+
+static error_t stop(Device* device) {
+ return ERROR_NONE;
+}
+
+// ---------------------------------------------------------------------------
+// Public API
+// ---------------------------------------------------------------------------
+
+extern "C" {
+
+error_t m5pm1_get_battery_voltage(Device* device, uint16_t* mv) {
+ return i2c_controller_register16le_get(device_get_parent(device), GET_CONFIG(device)->address, REG_VBAT_L, mv, TIMEOUT);
+}
+
+error_t m5pm1_get_vin_voltage(Device* device, uint16_t* mv) {
+ return i2c_controller_register16le_get(device_get_parent(device), GET_CONFIG(device)->address, REG_VIN_L, mv, TIMEOUT);
+}
+
+error_t m5pm1_get_5vout_voltage(Device* device, uint16_t* mv) {
+ return i2c_controller_register16le_get(device_get_parent(device), GET_CONFIG(device)->address, REG_5VOUT_L, mv, TIMEOUT);
+}
+
+error_t m5pm1_get_power_source(Device* device, M5pm1PowerSource* source) {
+ uint8_t val = 0;
+ error_t err = i2c_controller_register8_get(device_get_parent(device), GET_CONFIG(device)->address, REG_PWR_SRC, &val, TIMEOUT);
+ if (err != ERROR_NONE) return err;
+ *source = static_cast(val & 0x03U);
+ return ERROR_NONE;
+}
+
+error_t m5pm1_is_charging(Device* device, bool* charging) {
+ // PM1_G0 is wired to the charge IC's charge-status output: LOW = charging
+ uint8_t gpio_in = 0;
+ error_t err = i2c_controller_register8_get(device_get_parent(device), GET_CONFIG(device)->address, REG_GPIO_IN, &gpio_in, TIMEOUT);
+ if (err != ERROR_NONE) return err;
+ *charging = (gpio_in & 0x01U) == 0;
+ return ERROR_NONE;
+}
+
+error_t m5pm1_set_charge_enable(Device* device, bool enable) {
+ if (enable) {
+ return i2c_controller_register8_set_bits(device_get_parent(device), GET_CONFIG(device)->address, REG_PWR_CFG, PWR_CFG_CHG_EN, TIMEOUT);
+ } else {
+ return i2c_controller_register8_reset_bits(device_get_parent(device), GET_CONFIG(device)->address, REG_PWR_CFG, PWR_CFG_CHG_EN, TIMEOUT);
+ }
+}
+
+error_t m5pm1_set_boost_enable(Device* device, bool enable) {
+ if (enable) {
+ return i2c_controller_register8_set_bits(device_get_parent(device), GET_CONFIG(device)->address, REG_PWR_CFG, PWR_CFG_BOOST_EN, TIMEOUT);
+ } else {
+ return i2c_controller_register8_reset_bits(device_get_parent(device), GET_CONFIG(device)->address, REG_PWR_CFG, PWR_CFG_BOOST_EN, TIMEOUT);
+ }
+}
+
+error_t m5pm1_set_ldo_enable(Device* device, bool enable) {
+ if (enable) {
+ return i2c_controller_register8_set_bits(device_get_parent(device), GET_CONFIG(device)->address, REG_PWR_CFG, PWR_CFG_LDO_EN, TIMEOUT);
+ } else {
+ return i2c_controller_register8_reset_bits(device_get_parent(device), GET_CONFIG(device)->address, REG_PWR_CFG, PWR_CFG_LDO_EN, TIMEOUT);
+ }
+}
+
+error_t m5pm1_get_temperature(Device* device, uint16_t* decidegc) {
+ Device* i2c = device_get_parent(device);
+ uint8_t addr = GET_CONFIG(device)->address;
+
+ // Select temperature channel and start conversion
+ uint8_t ctrl = static_cast((ADC_CH_TEMP << 1U) | 0x01U);
+ error_t err = i2c_controller_register8_set(i2c, addr, REG_ADC_CTRL, ctrl, TIMEOUT);
+ if (err != ERROR_NONE) return err;
+
+ // Poll until conversion complete (START bit clears)
+ bool conversion_done = false;
+ for (int i = 0; i < 10; i++) {
+ vTaskDelay(pdMS_TO_TICKS(5));
+ uint8_t status = 0;
+ if (i2c_controller_register8_get(i2c, addr, REG_ADC_CTRL, &status, TIMEOUT) == ERROR_NONE) {
+ if ((status & 0x01U) == 0) {
+ conversion_done = true;
+ break;
+ }
+ }
+ }
+
+ if (!conversion_done) {
+ return ERROR_TIMEOUT;
+ }
+
+ return i2c_controller_register16le_get(i2c, addr, REG_ADC_RES_L, decidegc, TIMEOUT);
+}
+
+error_t m5pm1_shutdown(Device* device) {
+ uint8_t cmd = SYS_CMD_SHUTDOWN;
+ return i2c_controller_write_register(device_get_parent(device), GET_CONFIG(device)->address, REG_SYS_CMD, &cmd, 1, TIMEOUT);
+}
+
+error_t m5pm1_reboot(Device* device) {
+ uint8_t cmd = SYS_CMD_REBOOT;
+ return i2c_controller_write_register(device_get_parent(device), GET_CONFIG(device)->address, REG_SYS_CMD, &cmd, 1, TIMEOUT);
+}
+
+error_t m5pm1_btn_get_state(Device* device, bool* pressed) {
+ uint8_t val = 0;
+ error_t err = i2c_controller_register8_get(device_get_parent(device), GET_CONFIG(device)->address, REG_BTN_STATUS, &val, TIMEOUT);
+ if (err != ERROR_NONE) return err;
+ *pressed = (val & 0x01U) != 0;
+ return ERROR_NONE;
+}
+
+error_t m5pm1_btn_get_flag(Device* device, bool* was_pressed) {
+ uint8_t val = 0;
+ error_t err = i2c_controller_register8_get(device_get_parent(device), GET_CONFIG(device)->address, REG_BTN_STATUS, &val, TIMEOUT);
+ if (err != ERROR_NONE) return err;
+ *was_pressed = (val & 0x80U) != 0; // BTN_FLAG auto-clears on read
+ return ERROR_NONE;
+}
+
+error_t m5pm1_wdt_set(Device* device, uint8_t timeout_sec) {
+ return i2c_controller_register8_set(device_get_parent(device), GET_CONFIG(device)->address, REG_WDT_CNT, timeout_sec, TIMEOUT);
+}
+
+error_t m5pm1_wdt_feed(Device* device) {
+ return i2c_controller_register8_set(device_get_parent(device), GET_CONFIG(device)->address, REG_WDT_KEY, 0xA5, TIMEOUT);
+}
+
+error_t m5pm1_read_rtc_ram(Device* device, uint8_t offset, uint8_t* data, uint8_t len) {
+ if (offset + len > 32) return ERROR_INVALID_ARGUMENT;
+ return i2c_controller_read_register(device_get_parent(device), GET_CONFIG(device)->address, static_cast(REG_RTC_RAM + offset), data, len, TIMEOUT);
+}
+
+error_t m5pm1_write_rtc_ram(Device* device, uint8_t offset, const uint8_t* data, uint8_t len) {
+ if (offset + len > 32) return ERROR_INVALID_ARGUMENT;
+ return i2c_controller_write_register(device_get_parent(device), GET_CONFIG(device)->address, static_cast(REG_RTC_RAM + offset), data, len, TIMEOUT);
+}
+
+error_t m5pm1_set_led_count(Device* device, uint8_t count) {
+ if (count == 0 || count > 32) return ERROR_INVALID_ARGUMENT;
+ uint8_t val = count & 0x3FU;
+ return i2c_controller_register8_set(device_get_parent(device), GET_CONFIG(device)->address, REG_NEO_CFG, val, TIMEOUT);
+}
+
+error_t m5pm1_set_led_color(Device* device, uint8_t index, uint8_t r, uint8_t g, uint8_t b) {
+ if (index >= 32) return ERROR_INVALID_ARGUMENT;
+ Device* i2c = device_get_parent(device);
+ uint8_t addr = GET_CONFIG(device)->address;
+ // Store as RGB565: [15:11]=R5, [10:5]=G6, [4:0]=B5
+ uint16_t rgb565 = static_cast(((r >> 3U) << 11U) | ((g >> 2U) << 5U) | (b >> 3U));
+ uint8_t buf[2] = { static_cast(rgb565 & 0xFFU), static_cast(rgb565 >> 8U) };
+ return i2c_controller_write_register(i2c, addr, static_cast(REG_NEO_DATA + index * 2U), buf, 2, TIMEOUT);
+}
+
+error_t m5pm1_refresh_leds(Device* device) {
+ return i2c_controller_register8_set_bits(device_get_parent(device), GET_CONFIG(device)->address, REG_NEO_CFG, 0x40U, TIMEOUT);
+}
+
+error_t m5pm1_disable_leds(Device* device) {
+ // Set count to 1 and write black, then refresh
+ Device* i2c = device_get_parent(device);
+ uint8_t addr = GET_CONFIG(device)->address;
+ uint8_t black[2] = { 0, 0 };
+ error_t err = i2c_controller_write_register(i2c, addr, REG_NEO_DATA, black, 2, TIMEOUT);
+ if (err != ERROR_NONE) return err;
+ uint8_t cfg = 0x41U; // REFRESH | count=1
+ return i2c_controller_register8_set(i2c, addr, REG_NEO_CFG, cfg, TIMEOUT);
+}
+
+Driver m5pm1_driver = {
+ .name = "m5pm1",
+ .compatible = (const char*[]) { "m5stack,m5pm1", nullptr },
+ .start_device = start,
+ .stop_device = stop,
+ .api = nullptr,
+ .device_type = nullptr,
+ .owner = &m5pm1_module,
+ .internal = nullptr
+};
+
+} // extern "C"
diff --git a/Drivers/m5pm1-module/source/module.cpp b/Drivers/m5pm1-module/source/module.cpp
new file mode 100644
index 000000000..1c5fe6fb7
--- /dev/null
+++ b/Drivers/m5pm1-module/source/module.cpp
@@ -0,0 +1,34 @@
+// SPDX-License-Identifier: Apache-2.0
+#include
+#include
+#include
+
+extern "C" {
+
+extern Driver m5pm1_driver;
+
+static error_t start() {
+ /* We crash when construct fails, because if a single driver fails to construct,
+ * there is no guarantee that the previously constructed drivers can be destroyed */
+ check(driver_construct_add(&m5pm1_driver) == ERROR_NONE);
+ return ERROR_NONE;
+}
+
+static error_t stop() {
+ /* We crash when destruct fails, because if a single driver fails to destruct,
+ * there is no guarantee that the previously destroyed drivers can be recovered */
+ check(driver_remove_destruct(&m5pm1_driver) == ERROR_NONE);
+ return ERROR_NONE;
+}
+
+extern const ModuleSymbol m5pm1_module_symbols[];
+
+Module m5pm1_module = {
+ .name = "m5pm1",
+ .start = start,
+ .stop = stop,
+ .symbols = m5pm1_module_symbols,
+ .internal = nullptr
+};
+
+} // extern "C"
diff --git a/Drivers/m5pm1-module/source/symbols.c b/Drivers/m5pm1-module/source/symbols.c
new file mode 100644
index 000000000..76ed259ea
--- /dev/null
+++ b/Drivers/m5pm1-module/source/symbols.c
@@ -0,0 +1,28 @@
+// SPDX-License-Identifier: Apache-2.0
+#include
+#include
+
+const struct ModuleSymbol m5pm1_module_symbols[] = {
+ DEFINE_MODULE_SYMBOL(m5pm1_get_battery_voltage),
+ DEFINE_MODULE_SYMBOL(m5pm1_get_vin_voltage),
+ DEFINE_MODULE_SYMBOL(m5pm1_get_5vout_voltage),
+ DEFINE_MODULE_SYMBOL(m5pm1_get_power_source),
+ DEFINE_MODULE_SYMBOL(m5pm1_is_charging),
+ DEFINE_MODULE_SYMBOL(m5pm1_set_charge_enable),
+ DEFINE_MODULE_SYMBOL(m5pm1_set_boost_enable),
+ DEFINE_MODULE_SYMBOL(m5pm1_set_ldo_enable),
+ DEFINE_MODULE_SYMBOL(m5pm1_get_temperature),
+ DEFINE_MODULE_SYMBOL(m5pm1_shutdown),
+ DEFINE_MODULE_SYMBOL(m5pm1_reboot),
+ DEFINE_MODULE_SYMBOL(m5pm1_btn_get_state),
+ DEFINE_MODULE_SYMBOL(m5pm1_btn_get_flag),
+ DEFINE_MODULE_SYMBOL(m5pm1_wdt_set),
+ DEFINE_MODULE_SYMBOL(m5pm1_wdt_feed),
+ DEFINE_MODULE_SYMBOL(m5pm1_read_rtc_ram),
+ DEFINE_MODULE_SYMBOL(m5pm1_write_rtc_ram),
+ DEFINE_MODULE_SYMBOL(m5pm1_set_led_count),
+ DEFINE_MODULE_SYMBOL(m5pm1_set_led_color),
+ DEFINE_MODULE_SYMBOL(m5pm1_refresh_leds),
+ DEFINE_MODULE_SYMBOL(m5pm1_disable_leds),
+ MODULE_SYMBOL_TERMINATOR
+};
diff --git a/Drivers/mpu6886-module/CMakeLists.txt b/Drivers/mpu6886-module/CMakeLists.txt
new file mode 100644
index 000000000..cdbd590fd
--- /dev/null
+++ b/Drivers/mpu6886-module/CMakeLists.txt
@@ -0,0 +1,11 @@
+cmake_minimum_required(VERSION 3.20)
+
+include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake")
+
+file(GLOB_RECURSE SOURCE_FILES "source/*.c*")
+
+tactility_add_module(mpu6886-module
+ SRCS ${SOURCE_FILES}
+ INCLUDE_DIRS include/
+ REQUIRES TactilityKernel
+)
diff --git a/Drivers/mpu6886-module/LICENSE-Apache-2.0.md b/Drivers/mpu6886-module/LICENSE-Apache-2.0.md
new file mode 100644
index 000000000..f5f4b8b5e
--- /dev/null
+++ b/Drivers/mpu6886-module/LICENSE-Apache-2.0.md
@@ -0,0 +1,195 @@
+Apache License
+==============
+
+_Version 2.0, January 2004_
+_<>_
+
+### Terms and Conditions for use, reproduction, and distribution
+
+#### 1. Definitions
+
+“License” shall mean the terms and conditions for use, reproduction, and
+distribution as defined by Sections 1 through 9 of this document.
+
+“Licensor” shall mean the copyright owner or entity authorized by the copyright
+owner that is granting the License.
+
+“Legal Entity” shall mean the union of the acting entity and all other entities
+that control, are controlled by, or are under common control with that entity.
+For the purposes of this definition, “control” means **(i)** the power, direct or
+indirect, to cause the direction or management of such entity, whether by
+contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
+outstanding shares, or **(iii)** beneficial ownership of such entity.
+
+“You” (or “Your”) shall mean an individual or Legal Entity exercising
+permissions granted by this License.
+
+“Source” form shall mean the preferred form for making modifications, including
+but not limited to software source code, documentation source, and configuration
+files.
+
+“Object” form shall mean any form resulting from mechanical transformation or
+translation of a Source form, including but not limited to compiled object code,
+generated documentation, and conversions to other media types.
+
+“Work” shall mean the work of authorship, whether in Source or Object form, made
+available under the License, as indicated by a copyright notice that is included
+in or attached to the work (an example is provided in the Appendix below).
+
+“Derivative Works” shall mean any work, whether in Source or Object form, that
+is based on (or derived from) the Work and for which the editorial revisions,
+annotations, elaborations, or other modifications represent, as a whole, an
+original work of authorship. For the purposes of this License, Derivative Works
+shall not include works that remain separable from, or merely link (or bind by
+name) to the interfaces of, the Work and Derivative Works thereof.
+
+“Contribution” shall mean any work of authorship, including the original version
+of the Work and any modifications or additions to that Work or Derivative Works
+thereof, that is intentionally submitted to Licensor for inclusion in the Work
+by the copyright owner or by an individual or Legal Entity authorized to submit
+on behalf of the copyright owner. For the purposes of this definition,
+“submitted” means any form of electronic, verbal, or written communication sent
+to the Licensor or its representatives, including but not limited to
+communication on electronic mailing lists, source code control systems, and
+issue tracking systems that are managed by, or on behalf of, the Licensor for
+the purpose of discussing and improving the Work, but excluding communication
+that is conspicuously marked or otherwise designated in writing by the copyright
+owner as “Not a Contribution.”
+
+“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
+of whom a Contribution has been received by Licensor and subsequently
+incorporated within the Work.
+
+#### 2. Grant of Copyright License
+
+Subject to the terms and conditions of this License, each Contributor hereby
+grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
+irrevocable copyright license to reproduce, prepare Derivative Works of,
+publicly display, publicly perform, sublicense, and distribute the Work and such
+Derivative Works in Source or Object form.
+
+#### 3. Grant of Patent License
+
+Subject to the terms and conditions of this License, each Contributor hereby
+grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
+irrevocable (except as stated in this section) patent license to make, have
+made, use, offer to sell, sell, import, and otherwise transfer the Work, where
+such license applies only to those patent claims licensable by such Contributor
+that are necessarily infringed by their Contribution(s) alone or by combination
+of their Contribution(s) with the Work to which such Contribution(s) was
+submitted. If You institute patent litigation against any entity (including a
+cross-claim or counterclaim in a lawsuit) alleging that the Work or a
+Contribution incorporated within the Work constitutes direct or contributory
+patent infringement, then any patent licenses granted to You under this License
+for that Work shall terminate as of the date such litigation is filed.
+
+#### 4. Redistribution
+
+You may reproduce and distribute copies of the Work or Derivative Works thereof
+in any medium, with or without modifications, and in Source or Object form,
+provided that You meet the following conditions:
+
+* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
+this License; and
+* **(b)** You must cause any modified files to carry prominent notices stating that You
+changed the files; and
+* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
+all copyright, patent, trademark, and attribution notices from the Source form
+of the Work, excluding those notices that do not pertain to any part of the
+Derivative Works; and
+* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
+Derivative Works that You distribute must include a readable copy of the
+attribution notices contained within such NOTICE file, excluding those notices
+that do not pertain to any part of the Derivative Works, in at least one of the
+following places: within a NOTICE text file distributed as part of the
+Derivative Works; within the Source form or documentation, if provided along
+with the Derivative Works; or, within a display generated by the Derivative
+Works, if and wherever such third-party notices normally appear. The contents of
+the NOTICE file are for informational purposes only and do not modify the
+License. You may add Your own attribution notices within Derivative Works that
+You distribute, alongside or as an addendum to the NOTICE text from the Work,
+provided that such additional attribution notices cannot be construed as
+modifying the License.
+
+You may add Your own copyright statement to Your modifications and may provide
+additional or different license terms and conditions for use, reproduction, or
+distribution of Your modifications, or for any such Derivative Works as a whole,
+provided Your use, reproduction, and distribution of the Work otherwise complies
+with the conditions stated in this License.
+
+#### 5. Submission of Contributions
+
+Unless You explicitly state otherwise, any Contribution intentionally submitted
+for inclusion in the Work by You to the Licensor shall be under the terms and
+conditions of this License, without any additional terms or conditions.
+Notwithstanding the above, nothing herein shall supersede or modify the terms of
+any separate license agreement you may have executed with Licensor regarding
+such Contributions.
+
+#### 6. Trademarks
+
+This License does not grant permission to use the trade names, trademarks,
+service marks, or product names of the Licensor, except as required for
+reasonable and customary use in describing the origin of the Work and
+reproducing the content of the NOTICE file.
+
+#### 7. Disclaimer of Warranty
+
+Unless required by applicable law or agreed to in writing, Licensor provides the
+Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
+including, without limitation, any warranties or conditions of TITLE,
+NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
+solely responsible for determining the appropriateness of using or
+redistributing the Work and assume any risks associated with Your exercise of
+permissions under this License.
+
+#### 8. Limitation of Liability
+
+In no event and under no legal theory, whether in tort (including negligence),
+contract, or otherwise, unless required by applicable law (such as deliberate
+and grossly negligent acts) or agreed to in writing, shall any Contributor be
+liable to You for damages, including any direct, indirect, special, incidental,
+or consequential damages of any character arising as a result of this License or
+out of the use or inability to use the Work (including but not limited to
+damages for loss of goodwill, work stoppage, computer failure or malfunction, or
+any and all other commercial damages or losses), even if such Contributor has
+been advised of the possibility of such damages.
+
+#### 9. Accepting Warranty or Additional Liability
+
+While redistributing the Work or Derivative Works thereof, You may choose to
+offer, and charge a fee for, acceptance of support, warranty, indemnity, or
+other liability obligations and/or rights consistent with this License. However,
+in accepting such obligations, You may act only on Your own behalf and on Your
+sole responsibility, not on behalf of any other Contributor, and only if You
+agree to indemnify, defend, and hold each Contributor harmless for any liability
+incurred by, or claims asserted against, such Contributor by reason of your
+accepting any such warranty or additional liability.
+
+_END OF TERMS AND CONDITIONS_
+
+### APPENDIX: How to apply the Apache License to your work
+
+To apply the Apache License to your work, attach the following boilerplate
+notice, with the fields enclosed by brackets `[]` replaced with your own
+identifying information. (Don't include the brackets!) The text should be
+enclosed in the appropriate comment syntax for the file format. We also
+recommend that a file or class name and description of purpose be included on
+the same “printed page” as the copyright notice for easier identification within
+third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
diff --git a/Drivers/mpu6886-module/README.md b/Drivers/mpu6886-module/README.md
new file mode 100644
index 000000000..82d6721a3
--- /dev/null
+++ b/Drivers/mpu6886-module/README.md
@@ -0,0 +1,7 @@
+# MPU6886 I2C Driver
+
+A driver for the `MPU6886` 6-axis IMU.
+
+See https://m5stack.oss-cn-shenzhen.aliyuncs.com/resource/docs/datasheet/core/MPU-6886-000193%2Bv1.1_GHIC_en.pdf
+
+License: [Apache v2.0](LICENSE-Apache-2.0.md)
diff --git a/Drivers/mpu6886-module/bindings/invensense,mpu6886.yaml b/Drivers/mpu6886-module/bindings/invensense,mpu6886.yaml
new file mode 100644
index 000000000..9d9a700c2
--- /dev/null
+++ b/Drivers/mpu6886-module/bindings/invensense,mpu6886.yaml
@@ -0,0 +1,5 @@
+description: InvenSense (TDK) MPU-6886 6-axis IMU
+
+include: ["i2c-device.yaml"]
+
+compatible: "invensense,mpu6886"
diff --git a/Drivers/mpu6886-module/devicetree.yaml b/Drivers/mpu6886-module/devicetree.yaml
new file mode 100644
index 000000000..a07d6f334
--- /dev/null
+++ b/Drivers/mpu6886-module/devicetree.yaml
@@ -0,0 +1,3 @@
+dependencies:
+ - TactilityKernel
+bindings: bindings
diff --git a/Drivers/mpu6886-module/include/bindings/mpu6886.h b/Drivers/mpu6886-module/include/bindings/mpu6886.h
new file mode 100644
index 000000000..dbbf93a15
--- /dev/null
+++ b/Drivers/mpu6886-module/include/bindings/mpu6886.h
@@ -0,0 +1,15 @@
+// SPDX-License-Identifier: Apache-2.0
+#pragma once
+
+#include
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+DEFINE_DEVICETREE(mpu6886, struct Mpu6886Config)
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/Drivers/mpu6886-module/include/drivers/mpu6886.h b/Drivers/mpu6886-module/include/drivers/mpu6886.h
new file mode 100644
index 000000000..7de0a60ff
--- /dev/null
+++ b/Drivers/mpu6886-module/include/drivers/mpu6886.h
@@ -0,0 +1,33 @@
+// SPDX-License-Identifier: Apache-2.0
+#pragma once
+
+#include
+#include
+
+struct Device;
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct Mpu6886Config {
+ /** Address on bus */
+ uint8_t address;
+};
+
+struct Mpu6886Data {
+ float ax, ay, az; // acceleration in g (±8g range)
+ float gx, gy, gz; // angular rate in °/s (±2000°/s range)
+};
+
+/**
+ * Read accelerometer and gyroscope data.
+ * @param[in] device mpu6886 device
+ * @param[out] data Pointer to Mpu6886Data to populate
+ * @return ERROR_NONE on success
+ */
+error_t mpu6886_read(struct Device* device, struct Mpu6886Data* data);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/Drivers/mpu6886-module/include/mpu6886_module.h b/Drivers/mpu6886-module/include/mpu6886_module.h
new file mode 100644
index 000000000..5d05f2ae8
--- /dev/null
+++ b/Drivers/mpu6886-module/include/mpu6886_module.h
@@ -0,0 +1,14 @@
+// SPDX-License-Identifier: Apache-2.0
+#pragma once
+
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+extern struct Module mpu6886_module;
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/Drivers/mpu6886-module/source/module.cpp b/Drivers/mpu6886-module/source/module.cpp
new file mode 100644
index 000000000..044ea12db
--- /dev/null
+++ b/Drivers/mpu6886-module/source/module.cpp
@@ -0,0 +1,34 @@
+// SPDX-License-Identifier: Apache-2.0
+#include
+#include
+#include
+
+extern "C" {
+
+extern Driver mpu6886_driver;
+
+static error_t start() {
+ /* We crash when construct fails, because if a single driver fails to construct,
+ * there is no guarantee that the previously constructed drivers can be destroyed */
+ check(driver_construct_add(&mpu6886_driver) == ERROR_NONE);
+ return ERROR_NONE;
+}
+
+static error_t stop() {
+ /* We crash when destruct fails, because if a single driver fails to destruct,
+ * there is no guarantee that the previously destroyed drivers can be recovered */
+ check(driver_remove_destruct(&mpu6886_driver) == ERROR_NONE);
+ return ERROR_NONE;
+}
+
+extern const ModuleSymbol mpu6886_module_symbols[];
+
+Module mpu6886_module = {
+ .name = "mpu6886",
+ .start = start,
+ .stop = stop,
+ .symbols = mpu6886_module_symbols,
+ .internal = nullptr
+};
+
+} // extern "C"
diff --git a/Drivers/mpu6886-module/source/mpu6886.cpp b/Drivers/mpu6886-module/source/mpu6886.cpp
new file mode 100644
index 000000000..f55d0eed8
--- /dev/null
+++ b/Drivers/mpu6886-module/source/mpu6886.cpp
@@ -0,0 +1,158 @@
+// SPDX-License-Identifier: Apache-2.0
+#include
+#include
+#include
+#include
+#include
+
+#define TAG "MPU6886"
+
+// Register map
+static constexpr uint8_t REG_SMPLRT_DIV = 0x19; // sample rate divider
+static constexpr uint8_t REG_CONFIG = 0x1A; // DLPF config
+static constexpr uint8_t REG_GYRO_CONFIG = 0x1B; // gyro full-scale select
+static constexpr uint8_t REG_ACCEL_CONFIG = 0x1C; // accel full-scale select
+static constexpr uint8_t REG_ACCEL_CONFIG2 = 0x1D; // accel low-pass filter
+static constexpr uint8_t REG_INT_PIN_CFG = 0x37; // interrupt pin config
+static constexpr uint8_t REG_INT_ENABLE = 0x38; // interrupt enable
+static constexpr uint8_t REG_ACCEL_XOUT_H = 0x3B; // first accel output register
+static constexpr uint8_t REG_GYRO_XOUT_H = 0x43; // first gyro output register
+static constexpr uint8_t REG_USER_CTRL = 0x6A; // user control (DMP, FIFO, I2C)
+static constexpr uint8_t REG_PWR_MGMT_1 = 0x6B; // power management 1
+static constexpr uint8_t REG_PWR_MGMT_2 = 0x6C; // power management 2
+static constexpr uint8_t REG_FIFO_EN = 0x23; // FIFO enable
+static constexpr uint8_t REG_WHO_AM_I = 0x75; // chip ID — expect 0x19
+
+static constexpr uint8_t WHO_AM_I_VALUE = 0x19;
+
+// Configuration values
+// GYRO_CONFIG: FS_SEL=3 (±2000°/s), FCHOICE_B=00 → 0x18
+static constexpr uint8_t GYRO_CONFIG_VAL = 0x18;
+// ACCEL_CONFIG: AFS_SEL=2 (±8g) → 0x10
+static constexpr uint8_t ACCEL_CONFIG_VAL = 0x10;
+// CONFIG: DLPF_CFG=1 → gyro BW=176Hz, temp BW=188Hz → 0x01
+static constexpr uint8_t CONFIG_VAL = 0x01;
+// SMPLRT_DIV: sample rate = 1kHz / (1 + 5) = 166Hz → 0x05
+static constexpr uint8_t SMPLRT_DIV_VAL = 0x05;
+
+// Scaling: full-scale / 2^15
+static constexpr float ACCEL_SCALE = 8.0f / 32768.0f; // g per LSB (±8g)
+static constexpr float GYRO_SCALE = 2000.0f / 32768.0f; // °/s per LSB (±2000°/s)
+
+static constexpr TickType_t I2C_TIMEOUT_TICKS = pdMS_TO_TICKS(10);
+
+#define GET_CONFIG(device) (static_cast((device)->config))
+
+// region Driver lifecycle
+
+static error_t start(Device* device) {
+ auto* i2c_controller = device_get_parent(device);
+ if (device_get_type(i2c_controller) != &I2C_CONTROLLER_TYPE) {
+ LOG_E(TAG, "Parent is not an I2C controller");
+ return ERROR_RESOURCE;
+ }
+
+ auto address = GET_CONFIG(device)->address;
+
+ // Verify chip ID
+ uint8_t who_am_i = 0;
+ if (i2c_controller_register8_get(i2c_controller, address, REG_WHO_AM_I, &who_am_i, I2C_TIMEOUT_TICKS) != ERROR_NONE
+ || who_am_i != WHO_AM_I_VALUE) {
+ LOG_E(TAG, "WHO_AM_I mismatch: got 0x%02X, expected 0x%02X", who_am_i, WHO_AM_I_VALUE);
+ return ERROR_RESOURCE;
+ }
+
+ // Wake from sleep (clear all PWR_MGMT_1 bits)
+ if (i2c_controller_register8_set(i2c_controller, address, REG_PWR_MGMT_1, 0x00, I2C_TIMEOUT_TICKS) != ERROR_NONE) return ERROR_RESOURCE;
+ vTaskDelay(pdMS_TO_TICKS(10));
+
+ // Device reset (bit 7)
+ if (i2c_controller_register8_set(i2c_controller, address, REG_PWR_MGMT_1, 0x80, I2C_TIMEOUT_TICKS) != ERROR_NONE) return ERROR_RESOURCE;
+ vTaskDelay(pdMS_TO_TICKS(10));
+
+ // Select auto clock (CLKSEL=1: PLL with gyro reference when available)
+ if (i2c_controller_register8_set(i2c_controller, address, REG_PWR_MGMT_1, 0x01, I2C_TIMEOUT_TICKS) != ERROR_NONE) return ERROR_RESOURCE;
+ vTaskDelay(pdMS_TO_TICKS(10));
+
+ // Configure accel: ±8g
+ if (i2c_controller_register8_set(i2c_controller, address, REG_ACCEL_CONFIG, ACCEL_CONFIG_VAL, I2C_TIMEOUT_TICKS) != ERROR_NONE) return ERROR_RESOURCE;
+ // Configure gyro: ±2000°/s
+ if (i2c_controller_register8_set(i2c_controller, address, REG_GYRO_CONFIG, GYRO_CONFIG_VAL, I2C_TIMEOUT_TICKS) != ERROR_NONE) return ERROR_RESOURCE;
+ // DLPF: gyro BW=176Hz
+ if (i2c_controller_register8_set(i2c_controller, address, REG_CONFIG, CONFIG_VAL, I2C_TIMEOUT_TICKS) != ERROR_NONE) return ERROR_RESOURCE;
+ // Sample rate: 1kHz / (1+5) = 166Hz
+ if (i2c_controller_register8_set(i2c_controller, address, REG_SMPLRT_DIV, SMPLRT_DIV_VAL, I2C_TIMEOUT_TICKS) != ERROR_NONE) return ERROR_RESOURCE;
+ // Clear interrupt enables before reconfiguring
+ if (i2c_controller_register8_set(i2c_controller, address, REG_INT_ENABLE, 0x00, I2C_TIMEOUT_TICKS) != ERROR_NONE) return ERROR_RESOURCE;
+ // Accel low-pass filter: ACCEL_FCHOICE_B=0, A_DLPF_CFG=0
+ if (i2c_controller_register8_set(i2c_controller, address, REG_ACCEL_CONFIG2, 0x00, I2C_TIMEOUT_TICKS) != ERROR_NONE) return ERROR_RESOURCE;
+ // Disable DMP and FIFO
+ if (i2c_controller_register8_set(i2c_controller, address, REG_USER_CTRL, 0x00, I2C_TIMEOUT_TICKS) != ERROR_NONE) return ERROR_RESOURCE;
+ if (i2c_controller_register8_set(i2c_controller, address, REG_FIFO_EN, 0x00, I2C_TIMEOUT_TICKS) != ERROR_NONE) return ERROR_RESOURCE;
+ // Interrupt: active-high (ACTL=0), push-pull (OPEN=0), latched (LATCH_INT_EN=1), cleared on any read (INT_ANYRD_2CLEAR=1) → 0x30
+ if (i2c_controller_register8_set(i2c_controller, address, REG_INT_PIN_CFG, 0x30, I2C_TIMEOUT_TICKS) != ERROR_NONE) return ERROR_RESOURCE;
+ // Enable DATA_RDY interrupt
+ if (i2c_controller_register8_set(i2c_controller, address, REG_INT_ENABLE, 0x01, I2C_TIMEOUT_TICKS) != ERROR_NONE) return ERROR_RESOURCE;
+
+ return ERROR_NONE;
+}
+
+static error_t stop(Device* device) {
+ auto* i2c_controller = device_get_parent(device);
+ if (device_get_type(i2c_controller) != &I2C_CONTROLLER_TYPE) {
+ LOG_E(TAG, "Parent is not an I2C controller");
+ return ERROR_RESOURCE;
+ }
+
+ auto address = GET_CONFIG(device)->address;
+
+ // Put device to sleep (set SLEEP bit in PWR_MGMT_1)
+ if (i2c_controller_register8_set(i2c_controller, address, REG_PWR_MGMT_1, 0x40, I2C_TIMEOUT_TICKS) != ERROR_NONE) {
+ LOG_E(TAG, "Failed to put MPU6886 to sleep");
+ return ERROR_RESOURCE;
+ }
+
+ return ERROR_NONE;
+}
+
+// endregion
+
+extern "C" {
+
+error_t mpu6886_read(Device* device, Mpu6886Data* data) {
+ auto* i2c_controller = device_get_parent(device);
+ auto address = GET_CONFIG(device)->address;
+
+ // MPU6886 is big-endian (MSB first), unlike BMI270
+ auto toI16 = [](uint8_t hi, uint8_t lo) -> int16_t {
+ return static_cast(static_cast(hi) << 8 | lo);
+ };
+
+ // Burst read: accel (6) + temp (2) + gyro (6) = 14 bytes at 0x3B
+ uint8_t buf[14] = {};
+ error_t error = i2c_controller_read_register(i2c_controller, address, REG_ACCEL_XOUT_H, buf, sizeof(buf), I2C_TIMEOUT_TICKS);
+ if (error != ERROR_NONE) return error;
+
+ data->ax = toI16(buf[0], buf[1]) * ACCEL_SCALE;
+ data->ay = toI16(buf[2], buf[3]) * ACCEL_SCALE;
+ data->az = toI16(buf[4], buf[5]) * ACCEL_SCALE;
+ // buf[6..7] = temperature (skipped)
+ data->gx = toI16(buf[8], buf[9]) * GYRO_SCALE;
+ data->gy = toI16(buf[10], buf[11]) * GYRO_SCALE;
+ data->gz = toI16(buf[12], buf[13]) * GYRO_SCALE;
+
+ return ERROR_NONE;
+}
+
+Driver mpu6886_driver = {
+ .name = "mpu6886",
+ .compatible = (const char*[]) { "invensense,mpu6886", nullptr },
+ .start_device = start,
+ .stop_device = stop,
+ .api = nullptr,
+ .device_type = nullptr,
+ .owner = &mpu6886_module,
+ .internal = nullptr
+};
+
+} // extern "C"
diff --git a/Drivers/mpu6886-module/source/symbols.c b/Drivers/mpu6886-module/source/symbols.c
new file mode 100644
index 000000000..c94694fb9
--- /dev/null
+++ b/Drivers/mpu6886-module/source/symbols.c
@@ -0,0 +1,8 @@
+// SPDX-License-Identifier: Apache-2.0
+#include
+#include
+
+const struct ModuleSymbol mpu6886_module_symbols[] = {
+ DEFINE_MODULE_SYMBOL(mpu6886_read),
+ MODULE_SYMBOL_TERMINATOR
+};
diff --git a/Drivers/qmi8658-module/CMakeLists.txt b/Drivers/qmi8658-module/CMakeLists.txt
new file mode 100644
index 000000000..bad70971d
--- /dev/null
+++ b/Drivers/qmi8658-module/CMakeLists.txt
@@ -0,0 +1,11 @@
+cmake_minimum_required(VERSION 3.20)
+
+include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake")
+
+file(GLOB_RECURSE SOURCE_FILES "source/*.c*")
+
+tactility_add_module(qmi8658-module
+ SRCS ${SOURCE_FILES}
+ INCLUDE_DIRS include/
+ REQUIRES TactilityKernel
+)
diff --git a/Drivers/qmi8658-module/LICENSE-Apache-2.0.md b/Drivers/qmi8658-module/LICENSE-Apache-2.0.md
new file mode 100644
index 000000000..f5f4b8b5e
--- /dev/null
+++ b/Drivers/qmi8658-module/LICENSE-Apache-2.0.md
@@ -0,0 +1,195 @@
+Apache License
+==============
+
+_Version 2.0, January 2004_
+_<>_
+
+### Terms and Conditions for use, reproduction, and distribution
+
+#### 1. Definitions
+
+“License” shall mean the terms and conditions for use, reproduction, and
+distribution as defined by Sections 1 through 9 of this document.
+
+“Licensor” shall mean the copyright owner or entity authorized by the copyright
+owner that is granting the License.
+
+“Legal Entity” shall mean the union of the acting entity and all other entities
+that control, are controlled by, or are under common control with that entity.
+For the purposes of this definition, “control” means **(i)** the power, direct or
+indirect, to cause the direction or management of such entity, whether by
+contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
+outstanding shares, or **(iii)** beneficial ownership of such entity.
+
+“You” (or “Your”) shall mean an individual or Legal Entity exercising
+permissions granted by this License.
+
+“Source” form shall mean the preferred form for making modifications, including
+but not limited to software source code, documentation source, and configuration
+files.
+
+“Object” form shall mean any form resulting from mechanical transformation or
+translation of a Source form, including but not limited to compiled object code,
+generated documentation, and conversions to other media types.
+
+“Work” shall mean the work of authorship, whether in Source or Object form, made
+available under the License, as indicated by a copyright notice that is included
+in or attached to the work (an example is provided in the Appendix below).
+
+“Derivative Works” shall mean any work, whether in Source or Object form, that
+is based on (or derived from) the Work and for which the editorial revisions,
+annotations, elaborations, or other modifications represent, as a whole, an
+original work of authorship. For the purposes of this License, Derivative Works
+shall not include works that remain separable from, or merely link (or bind by
+name) to the interfaces of, the Work and Derivative Works thereof.
+
+“Contribution” shall mean any work of authorship, including the original version
+of the Work and any modifications or additions to that Work or Derivative Works
+thereof, that is intentionally submitted to Licensor for inclusion in the Work
+by the copyright owner or by an individual or Legal Entity authorized to submit
+on behalf of the copyright owner. For the purposes of this definition,
+“submitted” means any form of electronic, verbal, or written communication sent
+to the Licensor or its representatives, including but not limited to
+communication on electronic mailing lists, source code control systems, and
+issue tracking systems that are managed by, or on behalf of, the Licensor for
+the purpose of discussing and improving the Work, but excluding communication
+that is conspicuously marked or otherwise designated in writing by the copyright
+owner as “Not a Contribution.”
+
+“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
+of whom a Contribution has been received by Licensor and subsequently
+incorporated within the Work.
+
+#### 2. Grant of Copyright License
+
+Subject to the terms and conditions of this License, each Contributor hereby
+grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
+irrevocable copyright license to reproduce, prepare Derivative Works of,
+publicly display, publicly perform, sublicense, and distribute the Work and such
+Derivative Works in Source or Object form.
+
+#### 3. Grant of Patent License
+
+Subject to the terms and conditions of this License, each Contributor hereby
+grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
+irrevocable (except as stated in this section) patent license to make, have
+made, use, offer to sell, sell, import, and otherwise transfer the Work, where
+such license applies only to those patent claims licensable by such Contributor
+that are necessarily infringed by their Contribution(s) alone or by combination
+of their Contribution(s) with the Work to which such Contribution(s) was
+submitted. If You institute patent litigation against any entity (including a
+cross-claim or counterclaim in a lawsuit) alleging that the Work or a
+Contribution incorporated within the Work constitutes direct or contributory
+patent infringement, then any patent licenses granted to You under this License
+for that Work shall terminate as of the date such litigation is filed.
+
+#### 4. Redistribution
+
+You may reproduce and distribute copies of the Work or Derivative Works thereof
+in any medium, with or without modifications, and in Source or Object form,
+provided that You meet the following conditions:
+
+* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
+this License; and
+* **(b)** You must cause any modified files to carry prominent notices stating that You
+changed the files; and
+* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
+all copyright, patent, trademark, and attribution notices from the Source form
+of the Work, excluding those notices that do not pertain to any part of the
+Derivative Works; and
+* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
+Derivative Works that You distribute must include a readable copy of the
+attribution notices contained within such NOTICE file, excluding those notices
+that do not pertain to any part of the Derivative Works, in at least one of the
+following places: within a NOTICE text file distributed as part of the
+Derivative Works; within the Source form or documentation, if provided along
+with the Derivative Works; or, within a display generated by the Derivative
+Works, if and wherever such third-party notices normally appear. The contents of
+the NOTICE file are for informational purposes only and do not modify the
+License. You may add Your own attribution notices within Derivative Works that
+You distribute, alongside or as an addendum to the NOTICE text from the Work,
+provided that such additional attribution notices cannot be construed as
+modifying the License.
+
+You may add Your own copyright statement to Your modifications and may provide
+additional or different license terms and conditions for use, reproduction, or
+distribution of Your modifications, or for any such Derivative Works as a whole,
+provided Your use, reproduction, and distribution of the Work otherwise complies
+with the conditions stated in this License.
+
+#### 5. Submission of Contributions
+
+Unless You explicitly state otherwise, any Contribution intentionally submitted
+for inclusion in the Work by You to the Licensor shall be under the terms and
+conditions of this License, without any additional terms or conditions.
+Notwithstanding the above, nothing herein shall supersede or modify the terms of
+any separate license agreement you may have executed with Licensor regarding
+such Contributions.
+
+#### 6. Trademarks
+
+This License does not grant permission to use the trade names, trademarks,
+service marks, or product names of the Licensor, except as required for
+reasonable and customary use in describing the origin of the Work and
+reproducing the content of the NOTICE file.
+
+#### 7. Disclaimer of Warranty
+
+Unless required by applicable law or agreed to in writing, Licensor provides the
+Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
+including, without limitation, any warranties or conditions of TITLE,
+NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
+solely responsible for determining the appropriateness of using or
+redistributing the Work and assume any risks associated with Your exercise of
+permissions under this License.
+
+#### 8. Limitation of Liability
+
+In no event and under no legal theory, whether in tort (including negligence),
+contract, or otherwise, unless required by applicable law (such as deliberate
+and grossly negligent acts) or agreed to in writing, shall any Contributor be
+liable to You for damages, including any direct, indirect, special, incidental,
+or consequential damages of any character arising as a result of this License or
+out of the use or inability to use the Work (including but not limited to
+damages for loss of goodwill, work stoppage, computer failure or malfunction, or
+any and all other commercial damages or losses), even if such Contributor has
+been advised of the possibility of such damages.
+
+#### 9. Accepting Warranty or Additional Liability
+
+While redistributing the Work or Derivative Works thereof, You may choose to
+offer, and charge a fee for, acceptance of support, warranty, indemnity, or
+other liability obligations and/or rights consistent with this License. However,
+in accepting such obligations, You may act only on Your own behalf and on Your
+sole responsibility, not on behalf of any other Contributor, and only if You
+agree to indemnify, defend, and hold each Contributor harmless for any liability
+incurred by, or claims asserted against, such Contributor by reason of your
+accepting any such warranty or additional liability.
+
+_END OF TERMS AND CONDITIONS_
+
+### APPENDIX: How to apply the Apache License to your work
+
+To apply the Apache License to your work, attach the following boilerplate
+notice, with the fields enclosed by brackets `[]` replaced with your own
+identifying information. (Don't include the brackets!) The text should be
+enclosed in the appropriate comment syntax for the file format. We also
+recommend that a file or class name and description of purpose be included on
+the same “printed page” as the copyright notice for easier identification within
+third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
diff --git a/Drivers/qmi8658-module/README.md b/Drivers/qmi8658-module/README.md
new file mode 100644
index 000000000..38fa3d6ca
--- /dev/null
+++ b/Drivers/qmi8658-module/README.md
@@ -0,0 +1,7 @@
+# QMI8658 I2C Driver
+
+A driver for the `QMI8658` 6-axis IMU.
+
+See https://www.waveshare.net/w/upload/5/5f/QMI8658C.pdf
+
+License: [Apache v2.0](LICENSE-Apache-2.0.md)
diff --git a/Drivers/qmi8658-module/bindings/qst,qmi8658.yaml b/Drivers/qmi8658-module/bindings/qst,qmi8658.yaml
new file mode 100644
index 000000000..6182201af
--- /dev/null
+++ b/Drivers/qmi8658-module/bindings/qst,qmi8658.yaml
@@ -0,0 +1,5 @@
+description: QST QMI8658 6-axis IMU
+
+include: [ "i2c-device.yaml" ]
+
+compatible: "qst,qmi8658"
diff --git a/Drivers/qmi8658-module/devicetree.yaml b/Drivers/qmi8658-module/devicetree.yaml
new file mode 100644
index 000000000..a07d6f334
--- /dev/null
+++ b/Drivers/qmi8658-module/devicetree.yaml
@@ -0,0 +1,3 @@
+dependencies:
+ - TactilityKernel
+bindings: bindings
diff --git a/Drivers/qmi8658-module/include/bindings/qmi8658.h b/Drivers/qmi8658-module/include/bindings/qmi8658.h
new file mode 100644
index 000000000..1e083f70d
--- /dev/null
+++ b/Drivers/qmi8658-module/include/bindings/qmi8658.h
@@ -0,0 +1,15 @@
+// SPDX-License-Identifier: Apache-2.0
+#pragma once
+
+#include
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+DEFINE_DEVICETREE(qmi8658, struct Qmi8658Config)
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/Drivers/qmi8658-module/include/drivers/qmi8658.h b/Drivers/qmi8658-module/include/drivers/qmi8658.h
new file mode 100644
index 000000000..ff1ba67a6
--- /dev/null
+++ b/Drivers/qmi8658-module/include/drivers/qmi8658.h
@@ -0,0 +1,33 @@
+// SPDX-License-Identifier: Apache-2.0
+#pragma once
+
+#include
+#include
+
+struct Device;
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct Qmi8658Config {
+ /** I2C address (0x6A when SA0=low, 0x6B when SA0=high) */
+ uint8_t address;
+};
+
+struct Qmi8658Data {
+ float ax, ay, az; // acceleration in g (±8g range)
+ float gx, gy, gz; // angular rate in °/s (±2048°/s range)
+};
+
+/**
+ * Read accelerometer and gyroscope data.
+ * @param[in] device qmi8658 device
+ * @param[out] data Pointer to Qmi8658Data to populate
+ * @return ERROR_NONE on success
+ */
+error_t qmi8658_read(struct Device* device, struct Qmi8658Data* data);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/Drivers/qmi8658-module/include/qmi8658_module.h b/Drivers/qmi8658-module/include/qmi8658_module.h
new file mode 100644
index 000000000..95a715721
--- /dev/null
+++ b/Drivers/qmi8658-module/include/qmi8658_module.h
@@ -0,0 +1,14 @@
+// SPDX-License-Identifier: Apache-2.0
+#pragma once
+
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+extern struct Module qmi8658_module;
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/Drivers/qmi8658-module/source/module.cpp b/Drivers/qmi8658-module/source/module.cpp
new file mode 100644
index 000000000..5e2308c24
--- /dev/null
+++ b/Drivers/qmi8658-module/source/module.cpp
@@ -0,0 +1,34 @@
+// SPDX-License-Identifier: Apache-2.0
+#include
+#include
+#include
+
+extern "C" {
+
+extern Driver qmi8658_driver;
+
+static error_t start() {
+ /* We crash when construct fails, because if a single driver fails to construct,
+ * there is no guarantee that the previously constructed drivers can be destroyed */
+ check(driver_construct_add(&qmi8658_driver) == ERROR_NONE);
+ return ERROR_NONE;
+}
+
+static error_t stop() {
+ /* We crash when destruct fails, because if a single driver fails to destruct,
+ * there is no guarantee that the previously destroyed drivers can be recovered */
+ check(driver_remove_destruct(&qmi8658_driver) == ERROR_NONE);
+ return ERROR_NONE;
+}
+
+extern const ModuleSymbol qmi8658_module_symbols[];
+
+Module qmi8658_module = {
+ .name = "qmi8658",
+ .start = start,
+ .stop = stop,
+ .symbols = qmi8658_module_symbols,
+ .internal = nullptr
+};
+
+} // extern "C"
diff --git a/Drivers/qmi8658-module/source/qmi8658.cpp b/Drivers/qmi8658-module/source/qmi8658.cpp
new file mode 100644
index 000000000..38d47e472
--- /dev/null
+++ b/Drivers/qmi8658-module/source/qmi8658.cpp
@@ -0,0 +1,131 @@
+// SPDX-License-Identifier: Apache-2.0
+#include
+#include
+#include
+#include
+#include
+
+#define TAG "QMI8658"
+
+// Register map (QMI8658 datasheet)
+static constexpr uint8_t REG_WHO_AM_I = 0x00; // chip ID — expect 0x05
+static constexpr uint8_t REG_CTRL1 = 0x02; // serial interface config
+static constexpr uint8_t REG_CTRL2 = 0x03; // accel: range[6:4] + ODR[3:0]
+static constexpr uint8_t REG_CTRL3 = 0x04; // gyro: range[6:4] + ODR[3:0]
+static constexpr uint8_t REG_CTRL7 = 0x08; // sensor enable: bit0=accel, bit1=gyro
+static constexpr uint8_t REG_AX_L = 0x35; // first data register (accel X LSB)
+
+static constexpr uint8_t WHO_AM_I_VALUE = 0x05;
+
+// CTRL1: auto address increment (bit 6), little-endian (bit 5 = 0)
+static constexpr uint8_t CTRL1_VAL = 0x40;
+// CTRL2: accel ±8G (aFS=0x02 → bits[6:4]=010) + 1000Hz ODR (0x03 → bits[3:0]=0011) = 0x23
+static constexpr uint8_t CTRL2_VAL = 0x23;
+// CTRL3: gyro ±2048DPS (gFS=0x06 → bits[6:4]=110) + 1000Hz ODR (0x03 → bits[3:0]=0011) = 0x63
+static constexpr uint8_t CTRL3_VAL = 0x63;
+// CTRL7: enable accel (bit0) + gyro (bit1)
+static constexpr uint8_t CTRL7_ENABLE = 0x03;
+
+// Scaling: full-scale / 2^15
+static constexpr float ACCEL_SCALE = 8.0f / 32768.0f; // g per LSB (±8g) → 1/4096
+static constexpr float GYRO_SCALE = 2048.0f / 32768.0f; // °/s per LSB (±2048°/s) → 1/16
+
+static constexpr TickType_t I2C_TIMEOUT_TICKS = pdMS_TO_TICKS(10);
+
+#define GET_CONFIG(device) (static_cast((device)->config))
+
+// region Driver lifecycle
+
+static error_t start(Device* device) {
+ auto* i2c_controller = device_get_parent(device);
+ if (device_get_type(i2c_controller) != &I2C_CONTROLLER_TYPE) {
+ LOG_E(TAG, "Parent is not an I2C controller");
+ return ERROR_RESOURCE;
+ }
+
+ auto address = GET_CONFIG(device)->address;
+
+ // Verify chip ID
+ uint8_t who_am_i = 0;
+ if (i2c_controller_register8_get(i2c_controller, address, REG_WHO_AM_I, &who_am_i, I2C_TIMEOUT_TICKS) != ERROR_NONE
+ || who_am_i != WHO_AM_I_VALUE) {
+ LOG_E(TAG, "WHO_AM_I mismatch: got 0x%02X, expected 0x%02X", who_am_i, WHO_AM_I_VALUE);
+ return ERROR_RESOURCE;
+ }
+
+ // Disable all sensors during configuration
+ if (i2c_controller_register8_set(i2c_controller, address, REG_CTRL7, 0x00, I2C_TIMEOUT_TICKS) != ERROR_NONE) return ERROR_RESOURCE;
+
+ // Serial interface: auto address increment, little-endian
+ if (i2c_controller_register8_set(i2c_controller, address, REG_CTRL1, CTRL1_VAL, I2C_TIMEOUT_TICKS) != ERROR_NONE) return ERROR_RESOURCE;
+
+ // Accel: ±8g, 1000Hz ODR
+ if (i2c_controller_register8_set(i2c_controller, address, REG_CTRL2, CTRL2_VAL, I2C_TIMEOUT_TICKS) != ERROR_NONE) return ERROR_RESOURCE;
+
+ // Gyro: ±2048°/s, 1000Hz ODR
+ if (i2c_controller_register8_set(i2c_controller, address, REG_CTRL3, CTRL3_VAL, I2C_TIMEOUT_TICKS) != ERROR_NONE) return ERROR_RESOURCE;
+
+ // Enable accel + gyro
+ if (i2c_controller_register8_set(i2c_controller, address, REG_CTRL7, CTRL7_ENABLE, I2C_TIMEOUT_TICKS) != ERROR_NONE) return ERROR_RESOURCE;
+
+ return ERROR_NONE;
+}
+
+static error_t stop(Device* device) {
+ auto* i2c_controller = device_get_parent(device);
+ if (device_get_type(i2c_controller) != &I2C_CONTROLLER_TYPE) {
+ LOG_E(TAG, "Parent is not an I2C controller");
+ return ERROR_RESOURCE;
+ }
+
+ auto address = GET_CONFIG(device)->address;
+
+ // Put device to sleep
+ if (i2c_controller_register8_set(i2c_controller, address, REG_CTRL7, 0x00, I2C_TIMEOUT_TICKS) != ERROR_NONE) {
+ LOG_E(TAG, "Failed to put QMI8658 to sleep");
+ return ERROR_RESOURCE;
+ }
+
+ return ERROR_NONE;
+}
+
+// endregion
+
+extern "C" {
+
+error_t qmi8658_read(Device* device, Qmi8658Data* data) {
+ auto* i2c_controller = device_get_parent(device);
+ auto address = GET_CONFIG(device)->address;
+
+ // Burst-read 12 bytes starting at AX_L (0x35): accel X/Y/Z then gyro X/Y/Z
+ // QMI8658 is little-endian (LSB first), same as BMI270
+ uint8_t buf[12] = {};
+ error_t error = i2c_controller_read_register(i2c_controller, address, REG_AX_L, buf, sizeof(buf), I2C_TIMEOUT_TICKS);
+ if (error != ERROR_NONE) return error;
+
+ auto toI16 = [](uint8_t lo, uint8_t hi) -> int16_t {
+ return static_cast(static_cast(hi) << 8 | lo);
+ };
+
+ data->ax = toI16(buf[0], buf[1]) * ACCEL_SCALE;
+ data->ay = toI16(buf[2], buf[3]) * ACCEL_SCALE;
+ data->az = toI16(buf[4], buf[5]) * ACCEL_SCALE;
+ data->gx = toI16(buf[6], buf[7]) * GYRO_SCALE;
+ data->gy = toI16(buf[8], buf[9]) * GYRO_SCALE;
+ data->gz = toI16(buf[10], buf[11]) * GYRO_SCALE;
+
+ return ERROR_NONE;
+}
+
+Driver qmi8658_driver = {
+ .name = "qmi8658",
+ .compatible = (const char*[]) { "qst,qmi8658", nullptr },
+ .start_device = start,
+ .stop_device = stop,
+ .api = nullptr,
+ .device_type = nullptr,
+ .owner = &qmi8658_module,
+ .internal = nullptr
+};
+
+} // extern "C"
diff --git a/Drivers/qmi8658-module/source/symbols.c b/Drivers/qmi8658-module/source/symbols.c
new file mode 100644
index 000000000..47ba5e540
--- /dev/null
+++ b/Drivers/qmi8658-module/source/symbols.c
@@ -0,0 +1,8 @@
+// SPDX-License-Identifier: Apache-2.0
+#include
+#include
+
+const struct ModuleSymbol qmi8658_module_symbols[] = {
+ DEFINE_MODULE_SYMBOL(qmi8658_read),
+ MODULE_SYMBOL_TERMINATOR
+};
diff --git a/Drivers/rx8130ce-module/CMakeLists.txt b/Drivers/rx8130ce-module/CMakeLists.txt
new file mode 100644
index 000000000..229a4a09d
--- /dev/null
+++ b/Drivers/rx8130ce-module/CMakeLists.txt
@@ -0,0 +1,11 @@
+cmake_minimum_required(VERSION 3.20)
+
+include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake")
+
+file(GLOB_RECURSE SOURCE_FILES "source/*.c*")
+
+tactility_add_module(rx8130ce-module
+ SRCS ${SOURCE_FILES}
+ INCLUDE_DIRS include/
+ REQUIRES TactilityKernel
+)
diff --git a/Drivers/rx8130ce-module/LICENSE-Apache-2.0.md b/Drivers/rx8130ce-module/LICENSE-Apache-2.0.md
new file mode 100644
index 000000000..f5f4b8b5e
--- /dev/null
+++ b/Drivers/rx8130ce-module/LICENSE-Apache-2.0.md
@@ -0,0 +1,195 @@
+Apache License
+==============
+
+_Version 2.0, January 2004_
+_<>_
+
+### Terms and Conditions for use, reproduction, and distribution
+
+#### 1. Definitions
+
+“License” shall mean the terms and conditions for use, reproduction, and
+distribution as defined by Sections 1 through 9 of this document.
+
+“Licensor” shall mean the copyright owner or entity authorized by the copyright
+owner that is granting the License.
+
+“Legal Entity” shall mean the union of the acting entity and all other entities
+that control, are controlled by, or are under common control with that entity.
+For the purposes of this definition, “control” means **(i)** the power, direct or
+indirect, to cause the direction or management of such entity, whether by
+contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
+outstanding shares, or **(iii)** beneficial ownership of such entity.
+
+“You” (or “Your”) shall mean an individual or Legal Entity exercising
+permissions granted by this License.
+
+“Source” form shall mean the preferred form for making modifications, including
+but not limited to software source code, documentation source, and configuration
+files.
+
+“Object” form shall mean any form resulting from mechanical transformation or
+translation of a Source form, including but not limited to compiled object code,
+generated documentation, and conversions to other media types.
+
+“Work” shall mean the work of authorship, whether in Source or Object form, made
+available under the License, as indicated by a copyright notice that is included
+in or attached to the work (an example is provided in the Appendix below).
+
+“Derivative Works” shall mean any work, whether in Source or Object form, that
+is based on (or derived from) the Work and for which the editorial revisions,
+annotations, elaborations, or other modifications represent, as a whole, an
+original work of authorship. For the purposes of this License, Derivative Works
+shall not include works that remain separable from, or merely link (or bind by
+name) to the interfaces of, the Work and Derivative Works thereof.
+
+“Contribution” shall mean any work of authorship, including the original version
+of the Work and any modifications or additions to that Work or Derivative Works
+thereof, that is intentionally submitted to Licensor for inclusion in the Work
+by the copyright owner or by an individual or Legal Entity authorized to submit
+on behalf of the copyright owner. For the purposes of this definition,
+“submitted” means any form of electronic, verbal, or written communication sent
+to the Licensor or its representatives, including but not limited to
+communication on electronic mailing lists, source code control systems, and
+issue tracking systems that are managed by, or on behalf of, the Licensor for
+the purpose of discussing and improving the Work, but excluding communication
+that is conspicuously marked or otherwise designated in writing by the copyright
+owner as “Not a Contribution.”
+
+“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
+of whom a Contribution has been received by Licensor and subsequently
+incorporated within the Work.
+
+#### 2. Grant of Copyright License
+
+Subject to the terms and conditions of this License, each Contributor hereby
+grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
+irrevocable copyright license to reproduce, prepare Derivative Works of,
+publicly display, publicly perform, sublicense, and distribute the Work and such
+Derivative Works in Source or Object form.
+
+#### 3. Grant of Patent License
+
+Subject to the terms and conditions of this License, each Contributor hereby
+grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
+irrevocable (except as stated in this section) patent license to make, have
+made, use, offer to sell, sell, import, and otherwise transfer the Work, where
+such license applies only to those patent claims licensable by such Contributor
+that are necessarily infringed by their Contribution(s) alone or by combination
+of their Contribution(s) with the Work to which such Contribution(s) was
+submitted. If You institute patent litigation against any entity (including a
+cross-claim or counterclaim in a lawsuit) alleging that the Work or a
+Contribution incorporated within the Work constitutes direct or contributory
+patent infringement, then any patent licenses granted to You under this License
+for that Work shall terminate as of the date such litigation is filed.
+
+#### 4. Redistribution
+
+You may reproduce and distribute copies of the Work or Derivative Works thereof
+in any medium, with or without modifications, and in Source or Object form,
+provided that You meet the following conditions:
+
+* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
+this License; and
+* **(b)** You must cause any modified files to carry prominent notices stating that You
+changed the files; and
+* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
+all copyright, patent, trademark, and attribution notices from the Source form
+of the Work, excluding those notices that do not pertain to any part of the
+Derivative Works; and
+* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
+Derivative Works that You distribute must include a readable copy of the
+attribution notices contained within such NOTICE file, excluding those notices
+that do not pertain to any part of the Derivative Works, in at least one of the
+following places: within a NOTICE text file distributed as part of the
+Derivative Works; within the Source form or documentation, if provided along
+with the Derivative Works; or, within a display generated by the Derivative
+Works, if and wherever such third-party notices normally appear. The contents of
+the NOTICE file are for informational purposes only and do not modify the
+License. You may add Your own attribution notices within Derivative Works that
+You distribute, alongside or as an addendum to the NOTICE text from the Work,
+provided that such additional attribution notices cannot be construed as
+modifying the License.
+
+You may add Your own copyright statement to Your modifications and may provide
+additional or different license terms and conditions for use, reproduction, or
+distribution of Your modifications, or for any such Derivative Works as a whole,
+provided Your use, reproduction, and distribution of the Work otherwise complies
+with the conditions stated in this License.
+
+#### 5. Submission of Contributions
+
+Unless You explicitly state otherwise, any Contribution intentionally submitted
+for inclusion in the Work by You to the Licensor shall be under the terms and
+conditions of this License, without any additional terms or conditions.
+Notwithstanding the above, nothing herein shall supersede or modify the terms of
+any separate license agreement you may have executed with Licensor regarding
+such Contributions.
+
+#### 6. Trademarks
+
+This License does not grant permission to use the trade names, trademarks,
+service marks, or product names of the Licensor, except as required for
+reasonable and customary use in describing the origin of the Work and
+reproducing the content of the NOTICE file.
+
+#### 7. Disclaimer of Warranty
+
+Unless required by applicable law or agreed to in writing, Licensor provides the
+Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
+including, without limitation, any warranties or conditions of TITLE,
+NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
+solely responsible for determining the appropriateness of using or
+redistributing the Work and assume any risks associated with Your exercise of
+permissions under this License.
+
+#### 8. Limitation of Liability
+
+In no event and under no legal theory, whether in tort (including negligence),
+contract, or otherwise, unless required by applicable law (such as deliberate
+and grossly negligent acts) or agreed to in writing, shall any Contributor be
+liable to You for damages, including any direct, indirect, special, incidental,
+or consequential damages of any character arising as a result of this License or
+out of the use or inability to use the Work (including but not limited to
+damages for loss of goodwill, work stoppage, computer failure or malfunction, or
+any and all other commercial damages or losses), even if such Contributor has
+been advised of the possibility of such damages.
+
+#### 9. Accepting Warranty or Additional Liability
+
+While redistributing the Work or Derivative Works thereof, You may choose to
+offer, and charge a fee for, acceptance of support, warranty, indemnity, or
+other liability obligations and/or rights consistent with this License. However,
+in accepting such obligations, You may act only on Your own behalf and on Your
+sole responsibility, not on behalf of any other Contributor, and only if You
+agree to indemnify, defend, and hold each Contributor harmless for any liability
+incurred by, or claims asserted against, such Contributor by reason of your
+accepting any such warranty or additional liability.
+
+_END OF TERMS AND CONDITIONS_
+
+### APPENDIX: How to apply the Apache License to your work
+
+To apply the Apache License to your work, attach the following boilerplate
+notice, with the fields enclosed by brackets `[]` replaced with your own
+identifying information. (Don't include the brackets!) The text should be
+enclosed in the appropriate comment syntax for the file format. We also
+recommend that a file or class name and description of purpose be included on
+the same “printed page” as the copyright notice for easier identification within
+third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
diff --git a/Drivers/rx8130ce-module/README.md b/Drivers/rx8130ce-module/README.md
new file mode 100644
index 000000000..df37e8bc9
--- /dev/null
+++ b/Drivers/rx8130ce-module/README.md
@@ -0,0 +1,8 @@
+# RX8130CE I2C Driver
+
+A driver for the `RX8130CE` Realtime Clock.
+
+See https://download.epsondevice.com/td/pdf/brief/RX8130CE_en.pdf
+And: https://download.epsondevice.com/td/pdf/app/RX8130CE_en.pdf
+
+License: [Apache v2.0](LICENSE-Apache-2.0.md)
diff --git a/Drivers/rx8130ce-module/bindings/epson,rx8130ce.yaml b/Drivers/rx8130ce-module/bindings/epson,rx8130ce.yaml
new file mode 100644
index 000000000..5b832890e
--- /dev/null
+++ b/Drivers/rx8130ce-module/bindings/epson,rx8130ce.yaml
@@ -0,0 +1,5 @@
+description: Epson RX8130CE RTC
+
+include: [ "i2c-device.yaml" ]
+
+compatible: "epson,rx8130ce"
diff --git a/Drivers/rx8130ce-module/devicetree.yaml b/Drivers/rx8130ce-module/devicetree.yaml
new file mode 100644
index 000000000..a07d6f334
--- /dev/null
+++ b/Drivers/rx8130ce-module/devicetree.yaml
@@ -0,0 +1,3 @@
+dependencies:
+ - TactilityKernel
+bindings: bindings
diff --git a/Drivers/rx8130ce-module/include/bindings/rx8130ce.h b/Drivers/rx8130ce-module/include/bindings/rx8130ce.h
new file mode 100644
index 000000000..682ba35cd
--- /dev/null
+++ b/Drivers/rx8130ce-module/include/bindings/rx8130ce.h
@@ -0,0 +1,15 @@
+// SPDX-License-Identifier: Apache-2.0
+#pragma once
+
+#include
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+DEFINE_DEVICETREE(rx8130ce, struct Rx8130ceConfig)
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/Drivers/rx8130ce-module/include/drivers/rx8130ce.h b/Drivers/rx8130ce-module/include/drivers/rx8130ce.h
new file mode 100644
index 000000000..e348b556d
--- /dev/null
+++ b/Drivers/rx8130ce-module/include/drivers/rx8130ce.h
@@ -0,0 +1,45 @@
+// SPDX-License-Identifier: Apache-2.0
+#pragma once
+
+#include
+#include