Compare commits

...

4 Commits

Author SHA1 Message Date
Adolfo Reyna 9a540c644e feat(rlcd): Waveshare ESP32-S3-RLCD-4.2 support on latest main
- ST7305 driver with persistent 15KB DMA buffer (fix use-after-free +
  WDT lockup that caused freeze after flash)
- Reset timing fix: 150ms after reset, 50ms after init
- DTS: i2s0 mclk 16, bclk 9, ws 45, out 8, in 10 - fixes GPIO5 conflict
- i2c0 uses esp32-i2c-master, SDMMC 1-bit
- device.properties: colorDepth 16, fontSize 20, DefaultDark
- Amp GPIO46 init in initBoot
- Buttons: two-button control GPIO18 (cycle) + GPIO0 (select)

Base: origin/main f9453d89
2026-07-21 11:20:32 -04:00
Ken Van Hoeylandt 2fbc44466a Device migrations, driver migrations and more (#575) 2026-07-20 23:43:17 +02:00
Shadowtrance 2d768ef3a1 ESP-NOW bridge for P4 (#573) 2026-07-19 09:23:20 +02:00
reyna-b bd30aa046a Add ES3C28P device (#569) 2026-07-19 09:17:19 +02:00
274 changed files with 11021 additions and 3723 deletions
+3 -10
View File
@@ -1,19 +1,12 @@
#include <tactility/error.h>
#include <tactility/module.h> #include <tactility/module.h>
extern "C" { extern "C" {
static error_t start() {
return ERROR_NONE;
}
static error_t stop() {
return ERROR_NONE;
}
struct Module btt_panda_touch_module = { struct Module btt_panda_touch_module = {
.name = "btt-panda-touch", .name = "btt-panda-touch",
.start = start, .start = [] -> error_t { return ERROR_NONE; },
.stop = stop, .stop = [] -> error_t { return ERROR_NONE; },
.symbols = nullptr, .symbols = nullptr,
.internal = nullptr .internal = nullptr
}; };
+3 -10
View File
@@ -1,19 +1,12 @@
#include <tactility/error.h>
#include <tactility/module.h> #include <tactility/module.h>
extern "C" { extern "C" {
static error_t start() {
return ERROR_NONE;
}
static error_t stop() {
return ERROR_NONE;
}
Module cyd_2432s024c_module = { Module cyd_2432s024c_module = {
.name = "cyd-2432s024c", .name = "cyd-2432s024c",
.start = start, .start = [] -> error_t { return ERROR_NONE; },
.stop = stop, .stop = [] -> error_t { return ERROR_NONE; },
.symbols = nullptr, .symbols = nullptr,
.internal = nullptr .internal = nullptr
}; };
+3 -10
View File
@@ -1,19 +1,12 @@
#include <tactility/error.h>
#include <tactility/module.h> #include <tactility/module.h>
extern "C" { extern "C" {
static error_t start() {
return ERROR_NONE;
}
static error_t stop() {
return ERROR_NONE;
}
struct Module cyd_2432s024r_module = { struct Module cyd_2432s024r_module = {
.name = "cyd-2432s024r", .name = "cyd-2432s024r",
.start = start, .start = [] -> error_t { return ERROR_NONE; },
.stop = stop, .stop = [] -> error_t { return ERROR_NONE; },
.symbols = nullptr, .symbols = nullptr,
.internal = nullptr .internal = nullptr
}; };
+3 -10
View File
@@ -1,19 +1,12 @@
#include <tactility/error.h>
#include <tactility/module.h> #include <tactility/module.h>
extern "C" { extern "C" {
static error_t start() {
return ERROR_NONE;
}
static error_t stop() {
return ERROR_NONE;
}
struct Module cyd_2432s028r_module = { struct Module cyd_2432s028r_module = {
.name = "cyd-2432s028r", .name = "cyd-2432s028r",
.start = start, .start = [] -> error_t { return ERROR_NONE; },
.stop = stop, .stop = [] -> error_t { return ERROR_NONE; },
.symbols = nullptr, .symbols = nullptr,
.internal = nullptr .internal = nullptr
}; };
+3 -10
View File
@@ -1,19 +1,12 @@
#include <tactility/error.h>
#include <tactility/module.h> #include <tactility/module.h>
extern "C" { extern "C" {
static error_t start() {
return ERROR_NONE;
}
static error_t stop() {
return ERROR_NONE;
}
struct Module cyd_2432s028rv3_module = { struct Module cyd_2432s028rv3_module = {
.name = "cyd-2432s028rv3", .name = "cyd-2432s028rv3",
.start = start, .start = [] -> error_t { return ERROR_NONE; },
.stop = stop, .stop = [] -> error_t { return ERROR_NONE; },
.symbols = nullptr, .symbols = nullptr,
.internal = nullptr .internal = nullptr
}; };
+4 -11
View File
@@ -1,19 +1,12 @@
#include <tactility/error.h>
#include <tactility/module.h> #include <tactility/module.h>
extern "C" { extern "C" {
static error_t start() { Module cyd_2432s032c_module = {
return ERROR_NONE;
}
static error_t stop() {
return ERROR_NONE;
}
struct Module cyd_2432s032c_module = {
.name = "cyd-2432s032c", .name = "cyd-2432s032c",
.start = start, .start = [] -> error_t { return ERROR_NONE; },
.stop = stop, .stop = [] -> error_t { return ERROR_NONE; },
.symbols = nullptr, .symbols = nullptr,
.internal = nullptr .internal = nullptr
}; };
+4 -11
View File
@@ -1,19 +1,12 @@
#include <tactility/error.h>
#include <tactility/module.h> #include <tactility/module.h>
extern "C" { extern "C" {
static error_t start() { Module cyd_3248s035c_module = {
return ERROR_NONE;
}
static error_t stop() {
return ERROR_NONE;
}
struct Module cyd_3248s035c_module = {
.name = "cyd-3248s035c", .name = "cyd-3248s035c",
.start = start, .start = [] -> error_t { return ERROR_NONE; },
.stop = stop, .stop = [] -> error_t { return ERROR_NONE; },
.symbols = nullptr, .symbols = nullptr,
.internal = nullptr .internal = nullptr
}; };
+4 -11
View File
@@ -1,19 +1,12 @@
#include <tactility/error.h>
#include <tactility/module.h> #include <tactility/module.h>
extern "C" { extern "C" {
static error_t start() { Module cyd_4848s040c_module = {
return ERROR_NONE;
}
static error_t stop() {
return ERROR_NONE;
}
struct Module cyd_4848s040c_module = {
.name = "cyd-4848s040c", .name = "cyd-4848s040c",
.start = start, .start = [] -> error_t { return ERROR_NONE; },
.stop = stop, .stop = [] -> error_t { return ERROR_NONE; },
.symbols = nullptr, .symbols = nullptr,
.internal = nullptr .internal = nullptr
}; };
+4 -11
View File
@@ -1,19 +1,12 @@
#include <tactility/error.h>
#include <tactility/module.h> #include <tactility/module.h>
extern "C" { extern "C" {
static error_t start() { Module cyd_8048s043c_module = {
return ERROR_NONE;
}
static error_t stop() {
return ERROR_NONE;
}
struct Module cyd_8048s043c_module = {
.name = "cyd-8048s043c", .name = "cyd-8048s043c",
.start = start, .start = [] -> error_t { return ERROR_NONE; },
.stop = stop, .stop = [] -> error_t { return ERROR_NONE; },
.symbols = nullptr, .symbols = nullptr,
.internal = nullptr .internal = nullptr
}; };
+4 -11
View File
@@ -1,19 +1,12 @@
#include <tactility/error.h>
#include <tactility/module.h> #include <tactility/module.h>
extern "C" { extern "C" {
static error_t start() { Module cyd_e32r28t_module = {
return ERROR_NONE;
}
static error_t stop() {
return ERROR_NONE;
}
struct Module cyd_e32r28t_module = {
.name = "cyd-e32r28t", .name = "cyd-e32r28t",
.start = start, .start = [] -> error_t { return ERROR_NONE; },
.stop = stop, .stop = [] -> error_t { return ERROR_NONE; },
.symbols = nullptr, .symbols = nullptr,
.internal = nullptr .internal = nullptr
}; };
+4 -11
View File
@@ -1,19 +1,12 @@
#include <tactility/error.h>
#include <tactility/module.h> #include <tactility/module.h>
extern "C" { extern "C" {
static error_t start() { Module cyd_e32r32p_module = {
return ERROR_NONE;
}
static error_t stop() {
return ERROR_NONE;
}
struct Module cyd_e32r32p_module = {
.name = "cyd-e32r32p", .name = "cyd-e32r32p",
.start = start, .start = [] -> error_t { return ERROR_NONE; },
.stop = stop, .stop = [] -> error_t { return ERROR_NONE; },
.symbols = nullptr, .symbols = nullptr,
.internal = nullptr .internal = nullptr
}; };
@@ -1,19 +1,12 @@
#include <tactility/error.h>
#include <tactility/module.h> #include <tactility/module.h>
extern "C" { extern "C" {
static error_t start() { Module elecrow_crowpanel_advance_28_module = {
return ERROR_NONE;
}
static error_t stop() {
return ERROR_NONE;
}
struct Module elecrow_crowpanel_advance_28_module = {
.name = "elecrow-crowpanel-advance-28", .name = "elecrow-crowpanel-advance-28",
.start = start, .start = [] -> error_t { return ERROR_NONE; },
.stop = stop, .stop = [] -> error_t { return ERROR_NONE; },
.symbols = nullptr .symbols = nullptr
}; };
@@ -1,19 +1,12 @@
#include <tactility/error.h>
#include <tactility/module.h> #include <tactility/module.h>
extern "C" { extern "C" {
static error_t start() {
return ERROR_NONE;
}
static error_t stop() {
return ERROR_NONE;
}
Module elecrow_crowpanel_advance_35_module = { Module elecrow_crowpanel_advance_35_module = {
.name = "elecrow-crowpanel-advance-35", .name = "elecrow-crowpanel-advance-35",
.start = start, .start = [] -> error_t { return ERROR_NONE; },
.stop = stop, .stop = [] -> error_t { return ERROR_NONE; },
.symbols = nullptr, .symbols = nullptr,
.internal = nullptr .internal = nullptr
}; };
@@ -1,19 +1,12 @@
#include <tactility/error.h>
#include <tactility/module.h> #include <tactility/module.h>
extern "C" { extern "C" {
static error_t start() { Module elecrow_crowpanel_advance_50_module = {
return ERROR_NONE;
}
static error_t stop() {
return ERROR_NONE;
}
struct Module elecrow_crowpanel_advance_50_module = {
.name = "elecrow-crowpanel-advance-50", .name = "elecrow-crowpanel-advance-50",
.start = start, .start = [] -> error_t { return ERROR_NONE; },
.stop = stop, .stop = [] -> error_t { return ERROR_NONE; },
.symbols = nullptr, .symbols = nullptr,
.internal = nullptr .internal = nullptr
}; };
@@ -1,19 +1,12 @@
#include <tactility/error.h>
#include <tactility/module.h> #include <tactility/module.h>
extern "C" { extern "C" {
static error_t start() { Module elecrow_crowpanel_basic_28_module = {
return ERROR_NONE;
}
static error_t stop() {
return ERROR_NONE;
}
struct Module elecrow_crowpanel_basic_28_module = {
.name = "elecrow-crowpanel-basic-28", .name = "elecrow-crowpanel-basic-28",
.start = start, .start = [] -> error_t { return ERROR_NONE; },
.stop = stop, .stop = [] -> error_t { return ERROR_NONE; },
.symbols = nullptr, .symbols = nullptr,
.internal = nullptr .internal = nullptr
}; };
@@ -1,19 +1,12 @@
#include <tactility/error.h>
#include <tactility/module.h> #include <tactility/module.h>
extern "C" { extern "C" {
static error_t start() {
return ERROR_NONE;
}
static error_t stop() {
return ERROR_NONE;
}
Module elecrow_crowpanel_basic_35_module = { Module elecrow_crowpanel_basic_35_module = {
.name = "elecrow-crowpanel-basic-35", .name = "elecrow-crowpanel-basic-35",
.start = start, .start = [] -> error_t { return ERROR_NONE; },
.stop = stop, .stop = [] -> error_t { return ERROR_NONE; },
.symbols = nullptr, .symbols = nullptr,
.internal = nullptr .internal = nullptr
}; };
@@ -1,19 +1,12 @@
#include <tactility/error.h>
#include <tactility/module.h> #include <tactility/module.h>
extern "C" { extern "C" {
static error_t start() { Module elecrow_crowpanel_basic_50_module = {
return ERROR_NONE;
}
static error_t stop() {
return ERROR_NONE;
}
struct Module elecrow_crowpanel_basic_50_module = {
.name = "elecrow-crowpanel-basic-50", .name = "elecrow-crowpanel-basic-50",
.start = start, .start = [] -> error_t { return ERROR_NONE; },
.stop = stop, .stop = [] -> error_t { return ERROR_NONE; },
.symbols = nullptr, .symbols = nullptr,
.internal = nullptr .internal = nullptr
}; };
+4
View File
@@ -0,0 +1,4 @@
idf_component_register(
SRCS "source/module.cpp"
REQUIRES TactilityKernel
)
+23
View File
@@ -0,0 +1,23 @@
general.vendor=CYD
general.name=ES3C28P
apps.launcherAppId=Launcher
hardware.target=ESP32S3
hardware.flashSize=16MB
hardware.spiRam=true
hardware.spiRamMode=OCT
hardware.spiRamSpeed=120M
hardware.esptoolFlashFreq=120M
hardware.tinyUsb=true
hardware.bluetooth=true
display.size=2.8"
display.shape=rectangle
display.dpi=143
lvgl.colorDepth=16
storage.userDataLocation=SD
dependencies.useDeprecatedHal=false
+7
View File
@@ -0,0 +1,7 @@
dependencies:
- Platforms/platform-esp32
- Drivers/ili9341-module
- Drivers/ft5x06-module
- Drivers/es8311-module
- Drivers/audio-stream-module
dts: es3c28p.dts
+142
View File
@@ -0,0 +1,142 @@
/dts-v1/;
#include <tactility/bindings/root.h>
#include <tactility/bindings/esp32_adc_oneshot.h>
#include <tactility/bindings/esp32_ble.h>
#include <tactility/bindings/esp32_gpio.h>
#include <tactility/bindings/esp32_i2c.h>
#include <tactility/bindings/esp32_i2s.h>
#include <tactility/bindings/esp32_pwm_ledc.h>
#include <tactility/bindings/esp32_sdmmc.h>
#include <tactility/bindings/esp32_spi.h>
#include <tactility/bindings/esp32_wifi_pinned.h>
#include <tactility/bindings/battery_sense.h>
#include <tactility/bindings/gpio_hog.h>
#include <tactility/bindings/pwm_backlight.h>
#include <bindings/ili9341.h>
#include <bindings/ft5x06.h>
#include <bindings/es8311.h>
/ {
compatible = "root";
model = "LCDWIKI ES3C28P";
wifi0 {
compatible = "espressif,esp32-wifi-pinned";
status = "disabled";
};
ble0 {
compatible = "espressif,esp32-ble";
status = "disabled";
};
gpio0 {
compatible = "espressif,esp32-gpio";
gpio-count = <49>;
};
/* FM8002E speaker amp enable, active-low on GPIO1 */
amp_enable {
compatible = "gpio-hog";
pin = <&gpio0 1 GPIO_FLAG_NONE>;
mode = <GPIO_HOG_MODE_OUTPUT_LOW>;
};
adc0 {
compatible = "espressif,esp32-adc-oneshot";
unit-id = <ADC_UNIT_1>;
clk-src = <ADC_RTC_CLK_SRC_DEFAULT>;
channels = <ADC_CHANNEL_8 ADC_ATTEN_DB_12 ADC_BITWIDTH_DEFAULT>;
};
/* BAT+ through 200K/200K divider into GPIO9 ADC1_CH8 */
battery-sense {
compatible = "battery-sense";
io-channel = <&adc0 0>;
reference-voltage-mv = <3300>;
multiplier = <2000>;
};
/* i2s0 must be before i2c0 so codec can reference it */
i2s0 {
compatible = "espressif,esp32-i2s";
port = <I2S_NUM_0>;
pin-bclk = <&gpio0 5 GPIO_FLAG_NONE>;
pin-ws = <&gpio0 7 GPIO_FLAG_NONE>;
pin-data-out = <&gpio0 8 GPIO_FLAG_NONE>;
pin-data-in = <&gpio0 6 GPIO_FLAG_NONE>;
pin-mclk = <&gpio0 4 GPIO_FLAG_NONE>;
};
i2c0 {
compatible = "espressif,esp32-i2c";
port = <I2C_NUM_0>;
clock-frequency = <100000>;
pin-sda = <&gpio0 16 GPIO_FLAG_NONE>;
pin-scl = <&gpio0 15 GPIO_FLAG_NONE>;
ft5x06@38 {
compatible = "focaltech,ft5x06";
reg = <0x38>;
x-max = <240>;
y-max = <320>;
swap-xy;
mirror-x;
pin-reset = <&gpio0 18 GPIO_FLAG_NONE>;
pin-interrupt = <&gpio0 17 GPIO_FLAG_NONE>;
};
es8311: es8311@18 {
compatible = "everest,es8311";
reg = <0x18>;
i2s = <&i2s0>;
};
};
display_backlight_pwm {
compatible = "espressif,esp32-pwm-ledc";
pin = <&gpio0 45 GPIO_FLAG_NONE>;
period-ns = <500000>;
ledc-timer = <0>;
ledc-channel = <0>;
};
display_backlight {
compatible = "pwm-backlight";
status = "disabled";
pwm = <&display_backlight_pwm>;
};
spi0 {
compatible = "espressif,esp32-spi";
host = <SPI2_HOST>;
pin-mosi = <&gpio0 11 GPIO_FLAG_NONE>;
pin-sclk = <&gpio0 12 GPIO_FLAG_NONE>;
cs-gpios = <&gpio0 10 GPIO_FLAG_NONE>;
display@0 {
compatible = "ilitek,ili9341";
horizontal-resolution = <320>;
vertical-resolution = <240>;
swap-xy;
invert-color;
bgr-order;
pixel-clock-hz = <40000000>;
pin-dc = <&gpio0 46 GPIO_FLAG_NONE>;
backlight = <&display_backlight>;
};
};
sdmmc0 {
compatible = "espressif,esp32-sdmmc";
pin-clk = <&gpio0 38 GPIO_FLAG_NONE>;
pin-cmd = <&gpio0 40 GPIO_FLAG_NONE>;
pin-d0 = <&gpio0 39 GPIO_FLAG_NONE>;
pin-d1 = <&gpio0 41 GPIO_FLAG_NONE>;
pin-d2 = <&gpio0 48 GPIO_FLAG_NONE>;
pin-d3 = <&gpio0 47 GPIO_FLAG_NONE>;
slot = <SDMMC_HOST_SLOT_1>;
bus-width = <4>;
};
};
+14
View File
@@ -0,0 +1,14 @@
#include <tactility/error.h>
#include <tactility/module.h>
extern "C" {
Module es3c28p_module = {
.name = "es3c28p",
.start = [] -> error_t { return ERROR_NONE; },
.stop = [] -> error_t { return ERROR_NONE; },
.symbols = nullptr,
.internal = nullptr
};
}
+4 -11
View File
@@ -1,19 +1,12 @@
#include <tactility/error.h>
#include <tactility/module.h> #include <tactility/module.h>
extern "C" { extern "C" {
static error_t start() { Module generic_esp32_module = {
return ERROR_NONE;
}
static error_t stop() {
return ERROR_NONE;
}
struct Module generic_esp32_module = {
.name = "generic-esp32", .name = "generic-esp32",
.start = start, .start = [] -> error_t { return ERROR_NONE; },
.stop = stop, .stop = [] -> error_t { return ERROR_NONE; },
.symbols = nullptr, .symbols = nullptr,
.internal = nullptr .internal = nullptr
}; };
+4 -11
View File
@@ -1,19 +1,12 @@
#include <tactility/error.h>
#include <tactility/module.h> #include <tactility/module.h>
extern "C" { extern "C" {
static error_t start() { Module generic_esp32c6_module = {
return ERROR_NONE;
}
static error_t stop() {
return ERROR_NONE;
}
struct Module generic_esp32c6_module = {
.name = "generic-esp32c6", .name = "generic-esp32c6",
.start = start, .start = [] -> error_t { return ERROR_NONE; },
.stop = stop, .stop = [] -> error_t { return ERROR_NONE; },
.symbols = nullptr, .symbols = nullptr,
.internal = nullptr .internal = nullptr
}; };
+4 -11
View File
@@ -1,19 +1,12 @@
#include <tactility/error.h>
#include <tactility/module.h> #include <tactility/module.h>
extern "C" { extern "C" {
static error_t start() { Module generic_esp32p4_module = {
return ERROR_NONE;
}
static error_t stop() {
return ERROR_NONE;
}
struct Module generic_esp32p4_module = {
.name = "generic-esp32p4", .name = "generic-esp32p4",
.start = start, .start = [] -> error_t { return ERROR_NONE; },
.stop = stop, .stop = [] -> error_t { return ERROR_NONE; },
.symbols = nullptr, .symbols = nullptr,
.internal = nullptr .internal = nullptr
}; };
+4 -11
View File
@@ -1,19 +1,12 @@
#include <tactility/error.h>
#include <tactility/module.h> #include <tactility/module.h>
extern "C" { extern "C" {
static error_t start() { Module generic_esp32s3_module = {
return ERROR_NONE;
}
static error_t stop() {
return ERROR_NONE;
}
struct Module generic_esp32s3_module = {
.name = "generic-esp32s3", .name = "generic-esp32s3",
.start = start, .start = [] -> error_t { return ERROR_NONE; },
.stop = stop, .stop = [] -> error_t { return ERROR_NONE; },
.symbols = nullptr, .symbols = nullptr,
.internal = nullptr .internal = nullptr
}; };
@@ -1,19 +1,12 @@
#include <tactility/error.h>
#include <tactility/module.h> #include <tactility/module.h>
extern "C" { extern "C" {
static error_t start() {
return ERROR_NONE;
}
static error_t stop() {
return ERROR_NONE;
}
Module guition_jc1060p470ciwy_module = { Module guition_jc1060p470ciwy_module = {
.name = "guition-jc1060p470ciwy", .name = "guition-jc1060p470ciwy",
.start = start, .start = [] -> error_t { return ERROR_NONE; },
.stop = stop, .stop = [] -> error_t { return ERROR_NONE; },
.symbols = nullptr, .symbols = nullptr,
.internal = nullptr .internal = nullptr
}; };
+4 -11
View File
@@ -1,19 +1,12 @@
#include <tactility/error.h>
#include <tactility/module.h> #include <tactility/module.h>
extern "C" { extern "C" {
static error_t start() { Module guition_jc2432w328c_module = {
return ERROR_NONE;
}
static error_t stop() {
return ERROR_NONE;
}
struct Module guition_jc2432w328c_module = {
.name = "guition-jc2432w328c", .name = "guition-jc2432w328c",
.start = start, .start = [] -> error_t { return ERROR_NONE; },
.stop = stop, .stop = [] -> error_t { return ERROR_NONE; },
.symbols = nullptr, .symbols = nullptr,
.internal = nullptr .internal = nullptr
}; };
+4 -11
View File
@@ -1,19 +1,12 @@
#include <tactility/error.h>
#include <tactility/module.h> #include <tactility/module.h>
extern "C" { extern "C" {
static error_t start() { Module guition_jc8048w550c_module = {
return ERROR_NONE;
}
static error_t stop() {
return ERROR_NONE;
}
struct Module guition_jc8048w550c_module = {
.name = "guition-jc8048w550c", .name = "guition-jc8048w550c",
.start = start, .start = [] -> error_t { return ERROR_NONE; },
.stop = stop, .stop = [] -> error_t { return ERROR_NONE; },
.symbols = nullptr, .symbols = nullptr,
.internal = nullptr .internal = nullptr
}; };
@@ -1,19 +1,12 @@
#include <tactility/error.h>
#include <tactility/module.h> #include <tactility/module.h>
extern "C" { extern "C" {
static error_t start() { Module heltec_wifi_lora_32_v3_module = {
return ERROR_NONE;
}
static error_t stop() {
return ERROR_NONE;
}
struct Module heltec_wifi_lora_32_v3_module = {
.name = "heltec-wifi-lora-32-v3", .name = "heltec-wifi-lora-32-v3",
.start = start, .start = [] -> error_t { return ERROR_NONE; },
.stop = stop, .stop = [] -> error_t { return ERROR_NONE; },
.symbols = nullptr, .symbols = nullptr,
.internal = nullptr .internal = nullptr
}; };
+4 -11
View File
@@ -1,19 +1,12 @@
#include <tactility/error.h>
#include <tactility/module.h> #include <tactility/module.h>
extern "C" { extern "C" {
static error_t start() { Module m5stack_stackchan_module = {
return ERROR_NONE;
}
static error_t stop() {
return ERROR_NONE;
}
struct Module m5stack_stackchan_module = {
.name = "m5stack-stackchan", .name = "m5stack-stackchan",
.start = start, .start = [] -> error_t { return ERROR_NONE; },
.stop = stop, .stop = [] -> error_t { return ERROR_NONE; },
.symbols = nullptr, .symbols = nullptr,
.internal = nullptr .internal = nullptr
}; };
+3 -3
View File
@@ -1,7 +1,7 @@
file(GLOB_RECURSE SOURCE_FILES Source/*.c*) file(GLOB_RECURSE SOURCE_FILES source/*.c*)
idf_component_register( idf_component_register(
SRCS ${SOURCE_FILES} SRCS ${SOURCE_FILES}
INCLUDE_DIRS "Source" INCLUDE_DIRS "source"
REQUIRES Tactility esp_lvgl_port esp_lcd ST7789 PwmBacklight ButtonControl vfs fatfs m5pm1-module REQUIRES TactilityKernel
) )
@@ -1,48 +0,0 @@
#include "devices/Display.h"
#include "devices/Power.h"
#include <tactility/device.h>
#include <drivers/m5pm1.h>
#include <Tactility/hal/Configuration.h>
#include <ButtonControl.h>
#include <PwmBacklight.h>
using namespace tt::hal;
static constexpr auto* TAG = "StickS3";
// Audio codec register programming is owned by the es8311-module driver (registered as an
// AUDIO_CODEC_TYPE device, see m5stack,sticks3.dts). This board still needs to drive the
// AW8737 speaker amplifier's enable line via m5pm1 (PM1_G3), which is board wiring glue
// rather than codec configuration, so it stays here.
static void enableSpeakerAmplifier() {
auto* m5pm1 = device_find_by_name("m5pm1");
if (m5pm1 == nullptr) {
LOG_W(TAG, "m5pm1 not found -- speaker amp not enabled");
return;
}
error_t error = m5pm1_set_speaker_enable(m5pm1, true);
if (error != ERROR_NONE) {
LOG_E(TAG, "Failed to enable speaker amplifier: %s", error_to_string(error));
}
}
bool initBoot() {
enableSpeakerAmplifier();
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
};
@@ -1,32 +0,0 @@
#include "Display.h"
#include <PwmBacklight.h>
#include <St7789Display.h>
std::shared_ptr<tt::hal::display::DisplayDevice> 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>(St7789Display::SpiConfiguration {
.spiHostDevice = LCD_SPI_HOST,
.csPin = LCD_PIN_CS,
.dcPin = LCD_PIN_DC,
.pixelClockFrequency = 40'000'000,
.transactionQueueDepth = 10
});
return std::make_shared<St7789Display>(panel_configuration, spi_configuration);
}
@@ -1,17 +0,0 @@
#pragma once
#include "Tactility/hal/display/DisplayDevice.h"
#include <memory>
#include <driver/gpio.h>
#include <driver/spi_common.h>
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<tt::hal::display::DisplayDevice> createDisplay();
@@ -1,94 +0,0 @@
#include "Power.h"
#include <Tactility/hal/power/PowerDevice.h>
#include <drivers/m5pm1.h>
#include <tactility/device.h>
#include <tactility/log.h>
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<float>(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<uint8_t>(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<PowerDevice> createPower() {
auto* m5pm1 = device_find_by_name("m5pm1");
if (m5pm1 == nullptr) {
LOG_E(TAG, "m5pm1 device not found");
}
return std::make_shared<StickS3Power>(m5pm1);
}
@@ -1,6 +0,0 @@
#pragma once
#include <memory>
#include <Tactility/hal/power/PowerDevice.h>
std::shared_ptr<tt::hal::power::PowerDevice> createPower();
@@ -12,6 +12,8 @@ hardware.esptoolFlashFreq=80M
hardware.tinyUsb=true hardware.tinyUsb=true
hardware.bluetooth=true hardware.bluetooth=true
dependencies.useDeprecatedHal=false
storage.userDataLocation=Internal storage.userDataLocation=Internal
display.size=1.14" display.size=1.14"
+2
View File
@@ -4,4 +4,6 @@ dependencies:
- Drivers/m5pm1-module - Drivers/m5pm1-module
- Drivers/es8311-module - Drivers/es8311-module
- Drivers/audio-stream-module - Drivers/audio-stream-module
- Drivers/st7789-module
- Drivers/button-control-module
dts: m5stack,sticks3.dts dts: m5stack,sticks3.dts
+40 -3
View File
@@ -11,7 +11,11 @@
#include <bindings/m5pm1.h> #include <bindings/m5pm1.h>
#include <bindings/es8311.h> #include <bindings/es8311.h>
#include <tactility/bindings/esp32_sdspi.h> #include <tactility/bindings/esp32_sdspi.h>
#include <tactility/bindings/display_placeholder.h> #include <tactility/bindings/esp32_pwm_ledc.h>
#include <tactility/bindings/pwm_backlight.h>
#include <bindings/st7789.h>
#include <bindings/button_control.h>
/ { / {
compatible = "root"; compatible = "root";
@@ -53,6 +57,8 @@
m5pm1 { m5pm1 {
compatible = "m5stack,m5pm1"; compatible = "m5stack,m5pm1";
reg = <0x6E>; reg = <0x6E>;
power-supply-reference-voltage-mv = <4200>;
speaker-amp-enable-at-boot;
}; };
bmi270 { bmi270 {
@@ -75,6 +81,22 @@
pin-scl = <&gpio0 10 GPIO_FLAG_NONE>; pin-scl = <&gpio0 10 GPIO_FLAG_NONE>;
}; };
display_backlight_pwm {
compatible = "espressif,esp32-pwm-ledc";
pin = <&gpio0 38 GPIO_FLAG_NONE>;
period-ns = <1953125>;
ledc-timer = <0>;
ledc-channel = <0>;
};
display_backlight {
compatible = "pwm-backlight";
// Off by default so display power-on won't show the screen from before the last power loss.
// The display backlight is turned on during the boot process.
status = "disabled";
pwm = <&display_backlight_pwm>;
};
spi0 { spi0 {
compatible = "espressif,esp32-spi"; compatible = "espressif,esp32-spi";
host = <SPI2_HOST>; host = <SPI2_HOST>;
@@ -82,11 +104,26 @@
pin-mosi = <&gpio0 39 GPIO_FLAG_NONE>; pin-mosi = <&gpio0 39 GPIO_FLAG_NONE>;
pin-sclk = <&gpio0 40 GPIO_FLAG_NONE>; pin-sclk = <&gpio0 40 GPIO_FLAG_NONE>;
display { display@0 {
compatible = "display-placeholder"; compatible = "sitronix,st7789";
horizontal-resolution = <135>;
vertical-resolution = <240>;
gap-x = <52>;
gap-y = <40>;
invert-color;
pixel-clock-hz = <40000000>;
pin-dc = <&gpio0 45 GPIO_FLAG_NONE>;
pin-reset = <&gpio0 21 GPIO_FLAG_NONE>;
backlight = <&display_backlight>;
}; };
}; };
buttons {
compatible = "tactility,button-control";
pin-primary = <&gpio0 11 GPIO_FLAG_NONE>;
pin-secondary = <&gpio0 12 GPIO_FLAG_NONE>;
};
uart_grove: uart1 { uart_grove: uart1 {
compatible = "espressif,esp32-uart"; compatible = "espressif,esp32-uart";
status = "disabled"; status = "disabled";
+14
View File
@@ -0,0 +1,14 @@
#include <tactility/error.h>
#include <tactility/module.h>
extern "C" {
Module m5stack_sticks3_module = {
.name = "m5stack-sticks3",
.start = [] -> error_t { return ERROR_NONE; },
.stop = [] -> error_t { return ERROR_NONE; },
.symbols = nullptr,
.internal = nullptr
};
}
+1 -1
View File
@@ -3,5 +3,5 @@ file(GLOB_RECURSE SOURCE_FILES Source/*.c*)
idf_component_register( idf_component_register(
SRCS ${SOURCE_FILES} SRCS ${SOURCE_FILES}
INCLUDE_DIRS "Source" INCLUDE_DIRS "Source"
REQUIRES Tactility esp_lvgl_port esp_lcd EspLcdCompat esp_lcd_ili9881c esp_lcd_st7123 esp_lcd_touch_st7123 GT911 PwmBacklight driver esp_driver_i2c vfs fatfs ina226-module REQUIRES Tactility esp_lvgl_port esp_lcd esp_lcd_ili9881c esp_lcd_st7123 esp_lcd_touch_st7123 esp_lcd_touch_gt911 driver esp_driver_i2c vfs fatfs ina226-module ili9881c-module st7123-module gt911-module
) )
@@ -1,205 +0,0 @@
#include "devices/Display.h"
#include "devices/Power.h"
#include "devices/Tab5Keyboard.h"
#include <tactility/drivers/gpio_controller.h>
#include <tactility/drivers/i2c_controller.h>
#include <Tactility/hal/Configuration.h>
#include <esp_clock_output.h>
using namespace tt::hal;
static constexpr auto* TAG = "Tab5";
static DeviceVector createDevices() {
auto* i2c2 = device_find_by_name("i2c2");
check(i2c2, "i2c2 not found");
return {
createPower(),
createDisplay(),
std::make_shared<Tab5Keyboard>(i2c2)
};
}
/*
PI4IOE5V6408-0 (0x43)
- Bit 0: RF internal/external switch
- Bit 1: Speaker enable
- Bit 2: External 5V bus enable
- Bit 3: /
- Bit 4: LCD reset
- Bit 5: Touch reset
- Bit 6: Camera reset
- Bit 7: Headphone detect
*/
constexpr auto GPIO_EXP0_PIN_RF_INTERNAL_EXTERNAL = 0;
constexpr auto GPIO_EXP0_PIN_SPEAKER_ENABLE = 1;
constexpr auto GPIO_EXP0_PIN_EXTERNAL_5V_BUS_ENABLE = 2;
constexpr auto GPIO_EXP0_PIN_LCD_RESET = 4;
constexpr auto GPIO_EXP0_PIN_TOUCH_RESET = 5;
constexpr auto GPIO_EXP0_PIN_CAMERA_RESET = 6;
constexpr auto GPIO_EXP0_PIN_HEADPHONE_DETECT = 7;
/*
PI4IOE5V6408-1 (0x44)
- Bit 0: C6 WLAN enable
- Bit 1: /
- Bit 2: /
- Bit 3: USB-A 5V enable
- Bit 4: Device power: PWROFF_PLUSE
- Bit 5: IP2326: nCHG_QC_EN
- Bit 6: IP2326: CHG_STAT_LED
- Bit 7: IP2326: CHG_EN
*/
constexpr auto GPIO_EXP1_PIN_C6_WLAN_ENABLE = 0;
constexpr auto GPIO_EXP1_PIN_USB_A_5V_ENABLE = 3;
constexpr auto GPIO_EXP1_PIN_DEVICE_POWER = 4;
constexpr auto GPIO_EXP1_PIN_IP2326_NCHG_QC_EN = 5;
constexpr auto GPIO_EXP1_PIN_IP2326_CHG_STAT_LED = 6;
constexpr auto GPIO_EXP1_PIN_IP2326_CHG_EN = 7;
static void initExpander0(::Device* io_expander0) {
auto* rf_pin = gpio_descriptor_acquire(io_expander0, GPIO_EXP0_PIN_RF_INTERNAL_EXTERNAL, GPIO_OWNER_GPIO);
check(rf_pin);
auto* speaker_enable_pin = gpio_descriptor_acquire(io_expander0, GPIO_EXP0_PIN_SPEAKER_ENABLE, GPIO_OWNER_GPIO);
check(speaker_enable_pin);
auto* external_5v_bus_enable_pin = gpio_descriptor_acquire(io_expander0, GPIO_EXP0_PIN_EXTERNAL_5V_BUS_ENABLE, GPIO_OWNER_GPIO);
check(external_5v_bus_enable_pin);
auto* lcd_reset_pin = gpio_descriptor_acquire(io_expander0, GPIO_EXP0_PIN_LCD_RESET, GPIO_OWNER_GPIO);
check(lcd_reset_pin);
auto* touch_reset_pin = gpio_descriptor_acquire(io_expander0, GPIO_EXP0_PIN_TOUCH_RESET, GPIO_OWNER_GPIO);
check(touch_reset_pin);
auto* camera_reset_pin = gpio_descriptor_acquire(io_expander0, GPIO_EXP0_PIN_CAMERA_RESET, GPIO_OWNER_GPIO);
check(camera_reset_pin);
auto* headphone_detect_pin = gpio_descriptor_acquire(io_expander0, GPIO_EXP0_PIN_HEADPHONE_DETECT, GPIO_OWNER_GPIO);
check(headphone_detect_pin);
gpio_descriptor_set_flags(rf_pin, GPIO_FLAG_DIRECTION_OUTPUT);
gpio_descriptor_set_flags(speaker_enable_pin, GPIO_FLAG_DIRECTION_OUTPUT);
gpio_descriptor_set_flags(external_5v_bus_enable_pin, GPIO_FLAG_DIRECTION_OUTPUT);
gpio_descriptor_set_flags(lcd_reset_pin, GPIO_FLAG_DIRECTION_OUTPUT);
gpio_descriptor_set_flags(touch_reset_pin, GPIO_FLAG_DIRECTION_OUTPUT);
gpio_descriptor_set_flags(camera_reset_pin, GPIO_FLAG_DIRECTION_OUTPUT);
gpio_descriptor_set_flags(headphone_detect_pin, GPIO_FLAG_DIRECTION_INPUT);
gpio_descriptor_set_level(rf_pin, false);
gpio_descriptor_set_level(speaker_enable_pin, false);
gpio_descriptor_set_level(external_5v_bus_enable_pin, true);
gpio_descriptor_set_level(lcd_reset_pin, false);
gpio_descriptor_set_level(touch_reset_pin, false);
gpio_descriptor_set_level(camera_reset_pin, true);
vTaskDelay(pdMS_TO_TICKS(10));
// Enable touch and lcd, but not the camera
gpio_descriptor_set_level(lcd_reset_pin, true);
gpio_descriptor_set_level(touch_reset_pin, true);
gpio_descriptor_release(rf_pin);
gpio_descriptor_release(speaker_enable_pin);
gpio_descriptor_release(external_5v_bus_enable_pin);
gpio_descriptor_release(lcd_reset_pin);
gpio_descriptor_release(touch_reset_pin);
gpio_descriptor_release(camera_reset_pin);
gpio_descriptor_release(headphone_detect_pin);
}
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);
gpio_descriptor_set_flags(device_power_pin, GPIO_FLAG_DIRECTION_OUTPUT);
gpio_descriptor_set_flags(ip2326_ncharge_qc_enable_pin, GPIO_FLAG_DIRECTION_OUTPUT);
gpio_descriptor_set_flags(ip2326_charge_state_led_pin, GPIO_FLAG_DIRECTION_OUTPUT);
gpio_descriptor_set_flags(ip2326_charge_enable_pin, GPIO_FLAG_DIRECTION_INPUT | GPIO_FLAG_PULL_UP);
gpio_descriptor_set_level(c6_wlan_enable_pin, true);
gpio_descriptor_set_level(usb_a_5v_enable_pin, true);
gpio_descriptor_set_level(device_power_pin, false);
gpio_descriptor_set_level(ip2326_ncharge_qc_enable_pin, false);
gpio_descriptor_set_level(ip2326_charge_state_led_pin, false);
gpio_descriptor_release(c6_wlan_enable_pin);
gpio_descriptor_release(usb_a_5v_enable_pin);
gpio_descriptor_release(device_power_pin);
gpio_descriptor_release(ip2326_ncharge_qc_enable_pin);
gpio_descriptor_release(ip2326_charge_state_led_pin);
gpio_descriptor_release(ip2326_charge_enable_pin);
}
// Audio codec register programming is owned by the es8388-module/es7210-module drivers
// (registered as AUDIO_CODEC_TYPE devices, see m5stack,tab5.dts). This board still needs
// to drive the external speaker amplifier's enable line via io_expander0, which is board
// wiring glue rather than codec configuration, so it stays here.
static error_t enableSpeakerAmplifier(::Device* io_expander0) {
auto* speaker_enable_pin = gpio_descriptor_acquire(io_expander0, GPIO_EXP0_PIN_SPEAKER_ENABLE, GPIO_OWNER_GPIO);
check(speaker_enable_pin, "Failed to acquire speaker enable pin");
error_t error = gpio_descriptor_set_level(speaker_enable_pin, true);
gpio_descriptor_release(speaker_enable_pin);
if (error != ERROR_NONE) {
LOG_E(TAG, "Failed to enable amplifier: %s", error_to_string(error));
return ERROR_RESOURCE;
}
return ERROR_NONE;
}
static esp_clock_output_mapping_handle_t camera_osc_handle = nullptr;
static void initCameraOsc() {
if (camera_osc_handle != nullptr) {
return;
}
// 24 MHz clock on GPIO 36 for the SC2356 MIPI CSI sensor, required before esp_video_init.
// Uses SPLL (480 MHz) via esp_clock_output with divider 20 = 24 MHz exactly.
// This avoids any LEDC clock source conflict with the display backlight.
if (esp_clock_output_start(CLKOUT_SIG_SPLL, GPIO_NUM_36, &camera_osc_handle) != ESP_OK) {
LOG_E(TAG, "Camera OSC clock output start failed");
return;
}
if (esp_clock_output_set_divider(camera_osc_handle, 20) != ESP_OK) {
LOG_E(TAG, "Camera OSC clock divider set failed");
esp_clock_output_stop(camera_osc_handle);
camera_osc_handle = nullptr;
return;
}
LOG_I(TAG, "Camera OSC 24MHz started on GPIO 36 (SPLL/20)");
}
static bool initBoot() {
auto* io_expander0 = device_find_by_name("io_expander0");
check(io_expander0, "io_expander0 not found");
auto* io_expander1 = device_find_by_name("io_expander1");
check(io_expander1, "io_expander1 not found");
initCameraOsc();
initExpander0(io_expander0);
initExpander1(io_expander1);
error_t error = enableSpeakerAmplifier(io_expander0);
if (error != ERROR_NONE) {
LOG_E(TAG, "Failed to enable speaker amplifier");
}
return true;
}
extern const Configuration hardwareConfiguration = {
.initBoot = initBoot,
.createDevices = createDevices
};
@@ -1,45 +0,0 @@
#include "Detect.h"
#include <tactility/device.h>
#include <tactility/drivers/i2c_controller.h>
#include <tactility/log.h>
#include <esp_lcd_touch_gt911.h>
#include <esp_lcd_touch_st7123.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
constexpr auto* TAG = "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) {
LOG_I(TAG, "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) {
LOG_I(TAG, "Detected ST7123 touch — using ST7123 display");
return Tab5Variant::St7123;
}
vTaskDelay(pdMS_TO_TICKS(100));
}
LOG_W(TAG, "No known touch controller detected, defaulting to ST7123");
return Tab5Variant::St7123;
}
@@ -1,8 +0,0 @@
#pragma once
enum class Tab5Variant {
Ili9881c_Gt911, // Older variant
St7123, // Newer variant (default)
};
[[nodiscard]] Tab5Variant detectVariant();
@@ -1,108 +0,0 @@
#include "Detect.h"
#include "Display.h"
#include "Ili9881cDisplay.h"
#include "St7123Display.h"
#include "St7123Touch.h"
#include <Gt911Touch.h>
#include <PwmBacklight.h>
#include <Tactility/hal/gpio/Gpio.h>
#include <tactility/check.h>
#include <tactility/device.h>
#include <tactility/log.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
constexpr auto* TAG = "Tab5Display";
// LCD reset is wired to the PI4IOE5V6408 IO expander (io_expander0, bit 4), pulsed in
// Configuration.cpp's initExpander0() before display creation - not a direct SoC GPIO.
constexpr auto LCD_PIN_RESET = GPIO_NUM_NC;
constexpr auto LCD_PIN_BACKLIGHT = GPIO_NUM_22;
static std::shared_ptr<tt::hal::touch::TouchDevice> createGt911Touch() {
auto* i2c = device_find_by_name("i2c0");
check(i2c);
auto configuration = std::make_unique<Gt911Touch::Configuration>(
i2c,
720,
1280,
false, // swapXY
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
);
return std::make_shared<Gt911Touch>(std::move(configuration));
}
static std::shared_ptr<tt::hal::touch::TouchDevice> createSt7123Touch() {
auto* i2c = device_find_by_name("i2c0");
check(i2c, "i2c0 not found");
auto configuration = std::make_unique<St7123Touch::Configuration>(
i2c,
720,
1280,
false, // swapXY
false, // mirrorX
false, // mirrorY
GPIO_NUM_23 // interrupt pin
);
return std::make_shared<St7123Touch>(std::move(configuration));
}
std::shared_ptr<tt::hal::display::DisplayDevice> createDisplay() {
// Initialize PWM backlight
if (!driver::pwmbacklight::init(LCD_PIN_BACKLIGHT, 5000, LEDC_TIMER_1, LEDC_CHANNEL_0)) {
LOG_W(TAG, "Failed to initialize backlight");
}
Tab5Variant variant = detectVariant();
std::shared_ptr<tt::hal::touch::TouchDevice> touch;
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>(EspLcdConfiguration {
.horizontalResolution = 720,
.verticalResolution = 1280,
.gapX = 0,
.gapY = 0,
.monochrome = false,
.swapXY = false,
.mirrorX = false,
.mirrorY = false,
.invertColor = false,
.bufferSize = 0, // 0 = default (1/10 of screen)
.swRotate = true,
.buffSpiram = true,
.touch = touch,
.backlightDutyFunction = driver::pwmbacklight::setBacklightDuty,
.resetPin = LCD_PIN_RESET,
.lvglColorFormat = LV_COLOR_FORMAT_RGB565,
.lvglSwapBytes = false,
.rgbElementOrder = LCD_RGB_ELEMENT_ORDER_RGB,
.bitsPerPixel = 16
});
if (variant == Tab5Variant::St7123) {
return std::static_pointer_cast<tt::hal::display::DisplayDevice>(
std::make_shared<St7123Display>(configuration)
);
} else {
return std::static_pointer_cast<tt::hal::display::DisplayDevice>(
std::make_shared<Ili9881cDisplay>(configuration)
);
}
}
@@ -1,154 +0,0 @@
#include "Ili9881cDisplay.h"
#include "ili9881_init_data.h"
#include <esp_lcd_ili9881c.h>
#include <tactility/log.h>
constexpr auto* TAG = "ILI9881C";
Ili9881cDisplay::~Ili9881cDisplay() {
// 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 Ili9881cDisplay::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) {
LOG_E(TAG, "Failed to acquire LDO channel for MIPI DSI PHY");
return false;
}
LOG_I(TAG, "Powered on");
// Create bus
// TODO: use MIPI_DSI_PHY_CLK_SRC_DEFAULT() in future ESP-IDF 6.0.0 update with esp_lcd_jd9165 library version 2.x
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 = 1000
};
if (esp_lcd_new_dsi_bus(&bus_config, &mipiDsiBus) != ESP_OK) {
LOG_E(TAG, "Failed to create bus");
esp_ldo_release_channel(ldoChannel);
ldoChannel = nullptr;
return false;
}
LOG_I(TAG, "Bus created");
return true;
}
bool Ili9881cDisplay::createIoHandle(esp_lcd_panel_io_handle_t& ioHandle) {
// Initialize MIPI DSI bus if not already done
if (mipiDsiBus == nullptr) {
if (!createMipiDsiBus()) {
return false;
}
}
// Use DBI interface to send LCD commands and parameters
esp_lcd_dbi_io_config_t dbi_config = ILI9881C_PANEL_IO_DBI_CONFIG();
if (esp_lcd_new_panel_io_dbi(mipiDsiBus, &dbi_config, &ioHandle) != ESP_OK) {
LOG_E(TAG, "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 Ili9881cDisplay::createPanelConfig(std::shared_ptr<EspLcdConfiguration> 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 = static_cast<uint8_t>(espLcdConfiguration->bitsPerPixel),
.flags = {
.reset_active_high = 0
},
.vendor_config = nullptr // Will be set in createPanelHandle
};
}
bool Ili9881cDisplay::createPanelHandle(esp_lcd_panel_io_handle_t ioHandle, const esp_lcd_panel_dev_config_t& panelConfig, esp_lcd_panel_handle_t& panelHandle) {
// Based on BSP: https://github.com/espressif/esp-bsp/blob/master/bsp/m5stack_tab5/README.md
// TODO: undo static
esp_lcd_dpi_panel_config_t dpi_config = {
.virtual_channel = 0,
.dpi_clk_src = MIPI_DSI_DPI_CLK_SRC_DEFAULT,
.dpi_clock_freq_mhz = 60,
.pixel_format = LCD_COLOR_PIXEL_FORMAT_RGB565,
.in_color_format = LCD_COLOR_FMT_RGB565,
.out_color_format = LCD_COLOR_FMT_RGB565,
.num_fbs = 2,
.video_timing =
{
.h_size = 720,
.v_size = 1280,
.hsync_pulse_width = 40,
.hsync_back_porch = 140,
.hsync_front_porch = 40,
.vsync_pulse_width = 4,
.vsync_back_porch = 20,
.vsync_front_porch = 20,
},
.flags = {
.use_dma2d = 1, // TODO: true?
.disable_lp = 0,
}
};
ili9881c_vendor_config_t vendor_config = {
.init_cmds = ili9881_init_data,
.init_cmds_size = std::size(ili9881_init_data),
.mipi_config = {
.dsi_bus = mipiDsiBus,
.dpi_config = &dpi_config,
.lane_num = 2,
},
};
// 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_ili9881c(ioHandle, &mutable_panel_config, &panelHandle) != ESP_OK) {
LOG_E(TAG, "Failed to create panel");
return false;
}
LOG_I(TAG, "Panel created successfully");
// Defer reset/init to base class applyConfiguration to avoid double initialization
return true;
}
lvgl_port_display_dsi_cfg_t Ili9881cDisplay::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,
},
};
}
@@ -1,38 +0,0 @@
#pragma once
#include <EspLcdDisplayV2.h>
#include <esp_lcd_mipi_dsi.h>
#include <esp_ldo_regulator.h>
class Ili9881cDisplay 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> 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:
Ili9881cDisplay(
const std::shared_ptr<EspLcdConfiguration>& configuration
) : EspLcdDisplayV2(configuration) {}
~Ili9881cDisplay() override;
std::string getName() const override { return "ILI9881C"; }
std::string getDescription() const override { return "ILI9881C MIPI-DSI display"; }
};
@@ -1,192 +0,0 @@
#include "Power.h"
#include <Tactility/hal/power/PowerDevice.h>
#include <drivers/ina226.h>
#include <tactility/device.h>
#include <tactility/drivers/gpio_controller.h>
#include <tactility/log.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
using namespace tt::hal::power;
static constexpr auto* TAG = "Tab5Power";
// NP-F550 is a 2S Li-ion pack; INA226 measures the pack voltage on BAT_IN
// before the DC-DC converter. Per-cell range 3.2-4.2V → pack range 6.4-8.4V.
static constexpr float MIN_BATTERY_VOLTAGE_MV = 6400.0f;
static constexpr float MAX_BATTERY_VOLTAGE_MV = 8400.0f;
// INA226 convention: negative raw current = charging, positive = discharging.
// After negation in getMetric(Current), >50mA means charging into the battery.
static constexpr float CHARGING_CURRENT_THRESHOLD_AMPS = 0.05f;
// GPIO expander 1 (0x44) pin 4: PWROFF_PULSE
static constexpr int GPIO_EXP1_PIN_DEVICE_POWER = 4;
// GPIO expander 1 (0x44) pin 5: IP2326 nCHG_QC_EN (active-low: LOW = QC enabled)
static constexpr int GPIO_EXP1_PIN_IP2326_NCHG_QC_EN = 5;
// GPIO expander 1 (0x44) pin 7: IP2326 CHG_EN (HIGH = charging enabled, LOW = disabled)
static constexpr int GPIO_EXP1_PIN_IP2326_CHG_EN = 7;
class Tab5Power final : public PowerDevice {
public:
Tab5Power(::Device* ina226Device, ::Device* ioExpander1Device)
: ina226(ina226Device), ioExpander1(ioExpander1Device) {
// Initialize CHG_EN as output HIGH (charging enabled at startup).
setAllowedToCharge(true);
}
std::string getName() const override { return "M5Stack Tab5 Power"; }
std::string getDescription() const override { return "Battery monitoring via INA226 over I2C"; }
bool supportsMetric(MetricType type) const override {
switch (type) {
using enum MetricType;
case BatteryVoltage:
case ChargeLevel:
case Current:
case IsCharging:
return ina226 != nullptr;
default:
return false;
}
}
bool getMetric(MetricType type, MetricData& data) override {
switch (type) {
using enum MetricType;
case BatteryVoltage: {
if (ina226 == nullptr) return false;
float volts = 0.0f;
if (ina226_read_bus_voltage(ina226, &volts) != ERROR_NONE) return false;
data.valueAsUint32 = static_cast<uint32_t>(volts * 1000.0f);
return true;
}
case ChargeLevel: {
if (ina226 == nullptr) return false;
float volts = 0.0f;
if (ina226_read_bus_voltage(ina226, &volts) != ERROR_NONE) return false;
float voltage_mv = volts * 1000.0f;
if (voltage_mv >= MAX_BATTERY_VOLTAGE_MV) {
data.valueAsUint8 = 100;
} else if (voltage_mv <= MIN_BATTERY_VOLTAGE_MV) {
data.valueAsUint8 = 0;
} else {
float factor = (voltage_mv - MIN_BATTERY_VOLTAGE_MV) / (MAX_BATTERY_VOLTAGE_MV - MIN_BATTERY_VOLTAGE_MV);
data.valueAsUint8 = static_cast<uint8_t>(factor * 100.0f);
}
return true;
}
case Current: {
if (ina226 == nullptr) return false;
float amps = 0.0f;
if (ina226_read_shunt_current(ina226, &amps) != ERROR_NONE) return false;
// INA226 convention: negative = charging, positive = discharging.
// Negate so the HAL value is positive when charging, negative when discharging.
data.valueAsInt32 = static_cast<int32_t>(-amps * 1000.0f);
return true;
}
case IsCharging: {
if (ina226 == nullptr) return false;
float amps = 0.0f;
if (ina226_read_shunt_current(ina226, &amps) != ERROR_NONE) return false;
// Raw INA226: negative = charging. Threshold in raw terms = -0.05A.
data.valueAsBool = amps < -CHARGING_CURRENT_THRESHOLD_AMPS;
return true;
}
default:
return false;
}
}
bool supportsChargeControl() const override { return ioExpander1 != nullptr; }
bool isAllowedToCharge() const override { return chargingAllowed; }
void setAllowedToCharge(bool allowed) override {
if (ioExpander1 == nullptr) return;
auto* pin = gpio_descriptor_acquire(ioExpander1, GPIO_EXP1_PIN_IP2326_CHG_EN, GPIO_OWNER_GPIO);
if (pin == nullptr) {
LOG_W(TAG, "Failed to acquire CHG_EN pin");
return;
}
if (gpio_descriptor_set_flags(pin, GPIO_FLAG_DIRECTION_OUTPUT) != ERROR_NONE) {
LOG_W(TAG, "Failed to set CHG_EN pin direction");
gpio_descriptor_release(pin);
return;
}
if (gpio_descriptor_set_level(pin, allowed) != ERROR_NONE) {
LOG_W(TAG, "Failed to set CHG_EN pin level");
gpio_descriptor_release(pin);
return;
}
gpio_descriptor_release(pin);
chargingAllowed = allowed;
}
bool supportsQuickCharge() const override { return ioExpander1 != nullptr; }
bool isQuickChargeEnabled() const override { return quickChargeEnabled; }
void setQuickChargeEnabled(bool enabled) override {
if (ioExpander1 == nullptr) return;
auto* pin = gpio_descriptor_acquire(ioExpander1, GPIO_EXP1_PIN_IP2326_NCHG_QC_EN, GPIO_OWNER_GPIO);
if (pin == nullptr) {
LOG_W(TAG, "Failed to acquire nCHG_QC_EN pin");
return;
}
if (gpio_descriptor_set_flags(pin, GPIO_FLAG_DIRECTION_OUTPUT) != ERROR_NONE) {
LOG_W(TAG, "Failed to set nCHG_QC_EN pin direction");
gpio_descriptor_release(pin);
return;
}
if (gpio_descriptor_set_level(pin, !enabled) != ERROR_NONE) {
LOG_W(TAG, "Failed to set nCHG_QC_EN pin level");
gpio_descriptor_release(pin);
return;
}
gpio_descriptor_release(pin);
quickChargeEnabled = enabled;
}
bool supportsPowerOff() const override { return ioExpander1 != nullptr; }
void powerOff() override {
if (ioExpander1 == nullptr) return;
auto* pin = gpio_descriptor_acquire(ioExpander1, GPIO_EXP1_PIN_DEVICE_POWER, GPIO_OWNER_GPIO);
if (pin == nullptr) {
LOG_E(TAG, "Failed to acquire DEVICE_POWER pin");
return;
}
for (int i = 0; i < 3; i++) {
gpio_descriptor_set_level(pin, true);
vTaskDelay(pdMS_TO_TICKS(100));
gpio_descriptor_set_level(pin, false);
vTaskDelay(pdMS_TO_TICKS(100));
}
gpio_descriptor_release(pin);
}
private:
::Device* ina226;
::Device* ioExpander1;
bool chargingAllowed = true;
bool quickChargeEnabled = false;
};
std::shared_ptr<PowerDevice> createPower() {
auto* ina226 = device_find_by_name("ina226");
if (ina226 == nullptr) {
LOG_E(TAG, "ina226 device not found");
}
auto* io_expander1 = device_find_by_name("io_expander1");
if (io_expander1 == nullptr) {
LOG_E(TAG, "io_expander1 not found");
}
return std::make_shared<Tab5Power>(ina226, io_expander1);
}
@@ -1,6 +0,0 @@
#pragma once
#include <memory>
#include <Tactility/hal/power/PowerDevice.h>
std::shared_ptr<tt::hal::power::PowerDevice> createPower();
@@ -1,148 +0,0 @@
#include "St7123Display.h"
#include "st7123_init_data.h"
#include <esp_lcd_st7123.h>
#include <tactility/log.h>
constexpr auto* TAG = "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) {
LOG_E(TAG, "Failed to acquire LDO channel for MIPI DSI PHY");
return false;
}
LOG_I(TAG, "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) {
LOG_E(TAG, "Failed to create bus");
esp_ldo_release_channel(ldoChannel);
ldoChannel = nullptr;
return false;
}
LOG_I(TAG, "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) {
LOG_E(TAG, "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> 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 = 2,
.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) {
LOG_E(TAG, "Failed to create panel");
return false;
}
LOG_I(TAG, "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,
},
};
}
@@ -1,38 +0,0 @@
#pragma once
#include <EspLcdDisplayV2.h>
#include <esp_lcd_mipi_dsi.h>
#include <esp_ldo_regulator.h>
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> 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<EspLcdConfiguration>& configuration
) : EspLcdDisplayV2(configuration) {}
~St7123Display() override;
std::string getName() const override { return "St7123"; }
std::string getDescription() const override { return "St7123 MIPI-DSI display"; }
};
@@ -1,41 +0,0 @@
#include "St7123Touch.h"
#include <tactility/drivers/esp32_i2c_master.h>
#include <tactility/log.h>
#include <esp_lcd_touch_st7123.h>
#include <esp_err.h>
constexpr auto* TAG = "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();
io_config.scl_speed_hz = esp32_i2c_master_get_clock_frequency(configuration->controller);
i2c_master_bus_handle_t bus = esp32_i2c_master_get_bus_handle(configuration->controller);
return esp_lcd_new_panel_io_i2c_v2(bus, &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
};
}
@@ -1,59 +0,0 @@
#pragma once
#include <EspLcdTouch.h>
#include <Tactility/TactilityCore.h>
#include <tactility/device.h>
class St7123Touch final : public EspLcdTouch {
public:
class Configuration {
public:
Configuration(
::Device* controller,
uint16_t xMax,
uint16_t yMax,
bool swapXy = false,
bool mirrorX = false,
bool mirrorY = false,
gpio_num_t pinInterrupt = GPIO_NUM_NC
) : controller(controller),
xMax(xMax),
yMax(yMax),
swapXy(swapXy),
mirrorX(mirrorX),
mirrorY(mirrorY),
pinInterrupt(pinInterrupt)
{}
::Device* controller;
uint16_t xMax;
uint16_t yMax;
bool swapXy;
bool mirrorX;
bool mirrorY;
gpio_num_t pinInterrupt;
};
private:
std::unique_ptr<Configuration> 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<Configuration> 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"; }
};
@@ -1,561 +0,0 @@
#include "Tab5Keyboard.h"
#include <Tactility/app/App.h>
#include <Tactility/lvgl/Keyboard.h>
#include <Tactility/lvgl/LvglSync.h>
#include <tactility/drivers/i2c_controller.h>
#include <tactility/log.h>
#include <esp_timer.h>
#include <lvgl.h>
// ---------------------------------------------------------------------------
// Register addresses
// ---------------------------------------------------------------------------
static constexpr uint8_t REG_INT_CFG = 0x00;
static constexpr uint8_t REG_INT_STAT = 0x01;
static constexpr uint8_t REG_EVENT_NUM = 0x02;
static constexpr uint8_t REG_BRIGHTNESS = 0x03;
static constexpr uint8_t REG_KEYBOARD_MODE = 0x10;
static constexpr uint8_t REG_RGB_MODE = 0x11;
static constexpr uint8_t REG_KEY_EVENT = 0x20;
static constexpr uint8_t REG_RGB_BASE = 0x60;
static constexpr uint8_t KEY_EVENT_EMPTY = 0xFF;
// ---------------------------------------------------------------------------
// Modifier key positions in the 5x14 matrix
// ---------------------------------------------------------------------------
static constexpr uint8_t MOD_ROW_SYM = 3, MOD_COL_SYM = 0;
static constexpr uint8_t MOD_ROW_AA = 3, MOD_COL_AA = 1;
static constexpr uint8_t MOD_ROW_CTRL = 4, MOD_COL_CTRL = 0;
static constexpr uint8_t MOD_ROW_ALT = 4, MOD_COL_ALT = 1;
// ---------------------------------------------------------------------------
// HID lookup tables
// Row-major: index = row * 14 + col, 5 rows x 14 cols = 70 entries.
// modifier 0x02 = Left Shift (pre-baked by firmware for shifted characters).
// ---------------------------------------------------------------------------
struct HidMapping {
uint8_t keycode;
uint8_t modifier;
};
static constexpr HidMapping KEY_MATRIX_HID_BASE[70] = {
// Row 0: Esc 1 2 3 4 5 6 7 8 9 0 - + Del
{0x29, 0x00}, {0x1E, 0x00}, {0x1F, 0x00}, {0x20, 0x00}, {0x21, 0x00}, {0x22, 0x00},
{0x23, 0x00}, {0x24, 0x00}, {0x25, 0x00}, {0x26, 0x00}, {0x27, 0x00}, {0x2D, 0x00},
{0x2E, 0x02}, {0x4C, 0x00},
// Row 1: ` ! @ # $ % ^ & * ( ) [ ] backslash
{0x35, 0x00}, {0x1E, 0x02}, {0x1F, 0x02}, {0x20, 0x02}, {0x21, 0x02}, {0x22, 0x02},
{0x23, 0x02}, {0x24, 0x02}, {0x25, 0x02}, {0x26, 0x02}, {0x27, 0x02}, {0x2F, 0x00},
{0x30, 0x00}, {0x31, 0x00},
// Row 2: Tab q w e r t y u i o p ; ' Backspace
{0x2B, 0x00}, {0x14, 0x00}, {0x1A, 0x00}, {0x08, 0x00}, {0x15, 0x00}, {0x17, 0x00},
{0x1C, 0x00}, {0x18, 0x00}, {0x0C, 0x00}, {0x12, 0x00}, {0x13, 0x00}, {0x33, 0x00},
{0x34, 0x00}, {0x2A, 0x00},
// Row 3: Sym Aa a s d f g h j k l ↑ _ Enter
{0x00, 0x00}, {0x00, 0x00}, {0x04, 0x00}, {0x16, 0x00}, {0x07, 0x00}, {0x09, 0x00},
{0x0A, 0x00}, {0x0B, 0x00}, {0x0D, 0x00}, {0x0E, 0x00}, {0x0F, 0x00}, {0x52, 0x00},
{0x2D, 0x02}, {0x28, 0x00},
// Row 4: Ctrl Alt z x c v b n m . ← ↓ → Space
{0x00, 0x00}, {0x00, 0x00}, {0x1D, 0x00}, {0x1B, 0x00}, {0x06, 0x00}, {0x19, 0x00},
{0x05, 0x00}, {0x11, 0x00}, {0x10, 0x00}, {0x37, 0x00}, {0x50, 0x00}, {0x51, 0x00},
{0x4F, 0x00}, {0x2C, 0x00},
};
static constexpr HidMapping KEY_MATRIX_HID_SYM[70] = {
// Row 0: identical to base
{0x29, 0x00}, {0x1E, 0x00}, {0x1F, 0x00}, {0x20, 0x00}, {0x21, 0x00}, {0x22, 0x00},
{0x23, 0x00}, {0x24, 0x00}, {0x25, 0x00}, {0x26, 0x00}, {0x27, 0x00}, {0x2D, 0x00},
{0x2E, 0x02}, {0x4C, 0x00},
// Row 1: Sym deltas: ` → ~, ! → ?, * → /, ( → <, ) → >, [ → {, ] → }, backslash → |
{0x35, 0x02}, {0x38, 0x02}, {0x1F, 0x02}, {0x20, 0x02}, {0x21, 0x02}, {0x22, 0x02},
{0x23, 0x02}, {0x24, 0x02}, {0x38, 0x00}, {0x36, 0x02}, {0x37, 0x02}, {0x2F, 0x02},
{0x30, 0x02}, {0x31, 0x02},
// Row 2: Sym deltas: ; → :, ' → "
{0x2B, 0x00}, {0x14, 0x00}, {0x1A, 0x00}, {0x08, 0x00}, {0x15, 0x00}, {0x17, 0x00},
{0x1C, 0x00}, {0x18, 0x00}, {0x0C, 0x00}, {0x12, 0x00}, {0x13, 0x00}, {0x33, 0x02},
{0x34, 0x02}, {0x2A, 0x00},
// Row 3: Sym delta: _ → =
{0x00, 0x00}, {0x00, 0x00}, {0x04, 0x00}, {0x16, 0x00}, {0x07, 0x00}, {0x09, 0x00},
{0x0A, 0x00}, {0x0B, 0x00}, {0x0D, 0x00}, {0x0E, 0x00}, {0x0F, 0x00}, {0x52, 0x00},
{0x2E, 0x00}, {0x28, 0x00},
// Row 4: Sym delta: . → ,
{0x00, 0x00}, {0x00, 0x00}, {0x1D, 0x00}, {0x1B, 0x00}, {0x06, 0x00}, {0x19, 0x00},
{0x05, 0x00}, {0x11, 0x00}, {0x10, 0x00}, {0x36, 0x00}, {0x50, 0x00}, {0x51, 0x00},
{0x4F, 0x00}, {0x2C, 0x00},
};
// ---------------------------------------------------------------------------
// HID usage code + modifier → LVGL key
// Covers all codes present in the Tab5 matrix tables above.
// ---------------------------------------------------------------------------
static uint32_t tab5TranslateKey(uint8_t keycode, uint8_t modifier, bool ctrl) {
const bool shift = (modifier & 0x22U) != 0U;
// Navigation → LVGL key constants
switch (keycode) {
case 0x29: return LV_KEY_ESC;
case 0x28: return LV_KEY_ENTER;
case 0x2A: return LV_KEY_BACKSPACE;
case 0x4C: return LV_KEY_DEL;
case 0x2B: return '\t';
// Arrows: Ctrl+arrow = focus navigation, plain arrow = raw cursor movement
case 0x52: return ctrl ? (uint32_t)LV_KEY_PREV : (uint32_t)LV_KEY_UP;
case 0x51: return ctrl ? (uint32_t)LV_KEY_NEXT : (uint32_t)LV_KEY_DOWN;
case 0x50: return ctrl ? (uint32_t)LV_KEY_PREV : (uint32_t)LV_KEY_LEFT;
case 0x4F: return ctrl ? (uint32_t)LV_KEY_NEXT : (uint32_t)LV_KEY_RIGHT;
default: break;
}
// Letters az / AZ
if (keycode >= 0x04U && keycode <= 0x1DU) {
uint32_t c = static_cast<uint32_t>('a' + (keycode - 0x04U));
return shift ? (c - 0x20U) : c;
}
// Numbers 10 and their shifted symbols
if (keycode >= 0x1EU && keycode <= 0x27U) {
static constexpr char nums[] = "1234567890";
static constexpr char snums[] = "!@#$%^&*()";
return shift ? static_cast<uint32_t>(snums[keycode - 0x1EU])
: static_cast<uint32_t>(nums[keycode - 0x1EU]);
}
// Space and punctuation - all codes present in the Tab5 matrix
switch (keycode) {
case 0x2C: return ' ';
case 0x2D: return shift ? '_' : '-';
case 0x2E: return shift ? '+' : '=';
case 0x2F: return shift ? '{' : '[';
case 0x30: return shift ? '}' : ']';
case 0x31: return shift ? '|' : '\\';
case 0x33: return shift ? ':' : ';';
case 0x34: return shift ? '"' : '\'';
case 0x35: return shift ? '~' : '`';
case 0x36: return shift ? '<' : ',';
case 0x37: return shift ? '>' : '.';
case 0x38: return shift ? '?' : '/';
default: return 0;
}
}
// ---------------------------------------------------------------------------
// I2C helpers - use Tactility I2C controller API
// ---------------------------------------------------------------------------
bool Tab5Keyboard::readReg(uint8_t reg, uint8_t& value) {
return i2c_controller_read_register(i2cController, I2C_ADDRESS, reg, &value, 1, pdMS_TO_TICKS(50)) == ERROR_NONE;
}
bool Tab5Keyboard::writeReg(uint8_t reg, uint8_t value) {
return i2c_controller_write_register(i2cController, I2C_ADDRESS, reg, &value, 1, pdMS_TO_TICKS(50)) == ERROR_NONE;
}
// ---------------------------------------------------------------------------
// LED helpers - LED0 = Sym indicator (green), LED1 = Aa indicator (red)
// RGB register layout: [B, G, R] per LED, stride 4 (byte 3 reserved)
// ---------------------------------------------------------------------------
void Tab5Keyboard::updateLeds() {
// [LED0: B,G,R, reserved, LED1: B,G,R]
uint8_t buf[7] = {
0x00, symActive ? uint8_t(0xA0) : uint8_t(0x00), 0x00, 0x00, // LED0: green if Sym
0x00, 0x00, aaSticky ? uint8_t(0xA0) : uint8_t(0x00), // LED1: red if Aa latched
};
// Write 7-byte block starting at REG_RGB_BASE
i2c_controller_write_register(i2cController, I2C_ADDRESS, REG_RGB_BASE, buf, 7, pdMS_TO_TICKS(50));
}
// ---------------------------------------------------------------------------
// IRQ pin - GPIO 50, active-low, falling edge
// ---------------------------------------------------------------------------
void IRAM_ATTR Tab5Keyboard::irqHandler(void* arg) {
auto* self = static_cast<Tab5Keyboard*>(arg);
self->irqPending = true;
}
bool Tab5Keyboard::configureIrqPin() {
gpio_config_t io_conf{};
io_conf.pin_bit_mask = (1ULL << INT_PIN);
io_conf.mode = GPIO_MODE_INPUT;
io_conf.pull_up_en = GPIO_PULLUP_ENABLE;
io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE;
io_conf.intr_type = GPIO_INTR_NEGEDGE;
if (gpio_config(&io_conf) != ESP_OK) {
return false;
}
const esp_err_t svc = gpio_install_isr_service(0);
if (svc != ESP_OK && svc != ESP_ERR_INVALID_STATE) {
return false;
}
if (gpio_isr_handler_add(INT_PIN, irqHandler, this) != ESP_OK) {
gpio_set_intr_type(INT_PIN, GPIO_INTR_DISABLE);
return false;
}
irqConfigured = true;
return true;
}
void Tab5Keyboard::removeIrqPin() {
if (!irqConfigured) return;
gpio_isr_handler_remove(INT_PIN);
irqConfigured = false;
irqPending = false;
}
// ---------------------------------------------------------------------------
// drainEvents - reads all pending events from the device queue
// ---------------------------------------------------------------------------
void Tab5Keyboard::drainEvents() {
uint8_t count = 0;
if (!readReg(REG_EVENT_NUM, count) || count == 0) {
return;
}
while (count > 0) {
uint8_t raw = 0;
if (!readReg(REG_KEY_EVENT, raw) || raw == KEY_EVENT_EMPTY) {
break;
}
const bool pressed = (raw & 0x80U) != 0U;
const uint8_t row = (raw >> 4U) & 0x07U;
const uint8_t col = raw & 0x0FU;
// Modifier keys: update state, no key output
if (row == MOD_ROW_SYM && col == MOD_COL_SYM) {
symActive = pressed;
updateLeds();
count--;
continue;
}
if (row == MOD_ROW_AA && col == MOD_COL_AA) {
if (pressed) {
aaHeld = true;
aaTapped = true; // assume tap until a real key is pressed while held
} else {
// Only latch sticky if no non-modifier key was pressed during this hold
if (aaTapped) {
aaSticky = !aaSticky;
}
aaHeld = false;
aaTapped = false;
}
updateLeds();
count--;
continue;
}
if (row == MOD_ROW_CTRL && col == MOD_COL_CTRL) {
ctrlHeld = pressed;
count--;
continue;
}
if (row == MOD_ROW_ALT && col == MOD_COL_ALT) {
count--;
continue;
}
if (row < 5U && col < 14U) {
const bool aaActive = aaHeld || aaSticky;
const HidMapping& m = symActive
? KEY_MATRIX_HID_SYM[row * 14U + col]
: KEY_MATRIX_HID_BASE[row * 14U + col];
if (m.keycode != 0U) {
const uint8_t modifier = static_cast<uint8_t>(m.modifier | (aaActive ? 0x02U : 0U));
const uint32_t lv_key = tab5TranslateKey(m.keycode, modifier, ctrlHeld);
if (lv_key != 0U) {
if (pressed) {
// A real key was pressed — this hold is a chord, not a tap
aaTapped = false;
if (lv_key == LV_KEY_ESC) {
tt::app::stop();
} else {
xQueueSend(queue, &lv_key, 0);
// Arm software repeat tracking by row/col to survive modifier changes
const uint32_t now_ms = static_cast<uint32_t>(esp_timer_get_time() / 1000);
repeatKey = lv_key;
repeatRow = row;
repeatCol = col;
repeatStartMs = now_ms;
repeatLastMs = 0;
// Consume sticky Aa after one keypress
if (aaSticky) {
aaSticky = false;
aaHeld = false;
updateLeds();
}
}
} else if (row == repeatRow && col == repeatCol) {
// Match release by position, not translated value — survives sticky Aa clear
repeatKey = 0;
}
}
}
}
count--;
}
// Clear INT status after draining so the line de-asserts
writeReg(REG_INT_STAT, 0x00);
}
// ---------------------------------------------------------------------------
// processKeyboard - called from 20ms Timer; IRQ-gated when INT is wired
// ---------------------------------------------------------------------------
void Tab5Keyboard::processKeyboard() {
bool shouldDrain = false;
if (irqConfigured) {
if (irqPending) {
irqPending = false;
shouldDrain = true;
}
} else {
// Polling: check INT_STA first — bit 0 = Normal mode event pending
uint8_t status = 0;
if (readReg(REG_INT_STAT, status) && (status & 0x01U)) {
shouldDrain = true;
}
}
if (shouldDrain) {
drainEvents();
}
// Software key-repeat (runs every tick regardless of IRQ)
if (repeatKey != 0U) {
const uint32_t now_ms = static_cast<uint32_t>(esp_timer_get_time() / 1000);
if ((now_ms - repeatStartMs) >= REPEAT_INITIAL_MS) {
const uint32_t last = repeatLastMs;
if (last == 0 || (now_ms - last) >= REPEAT_RATE_MS) {
repeatLastMs = now_ms;
xQueueSend(queue, &repeatKey, 0);
}
}
}
checkAttachState();
}
// ---------------------------------------------------------------------------
// applyAutoRotation - on attach, switches to landscape if not already (saving
// the prior rotation); on detach, restores the saved rotation if we were the
// ones who changed it. Only affects the live LVGL rotation, never persisted
// display settings.
// ---------------------------------------------------------------------------
bool Tab5Keyboard::applyAutoRotation(bool keyboardAttached) {
auto* display = lv_indev_get_display(kbHandle);
if (display == nullptr) {
return false;
}
if (!tt::lvgl::lock(pdMS_TO_TICKS(100))) {
return false; // retry next poll
}
if (keyboardAttached) {
if (lv_display_get_rotation(display) != LV_DISPLAY_ROTATION_90) {
savedRotation = lv_display_get_rotation(display);
rotationOverrideActive = true;
lv_display_set_rotation(display, LV_DISPLAY_ROTATION_90);
}
} else {
// Only restore if rotation is still what we set it to - if the user manually
// changed it since attaching, respect their choice instead.
if (rotationOverrideActive && lv_display_get_rotation(display) == LV_DISPLAY_ROTATION_90) {
lv_display_set_rotation(display, savedRotation);
}
rotationOverrideActive = false;
}
tt::lvgl::unlock();
return true;
}
// ---------------------------------------------------------------------------
// checkAttachState - throttled (~1s) hot-plug detection. Reapplies device
// register configuration and auto-rotation on detach/attach transitions.
// ---------------------------------------------------------------------------
void Tab5Keyboard::checkAttachState() {
static constexpr uint32_t ATTACH_CHECK_TICKS = 50; // ~1s at 20ms/tick
if (++attachCheckTickCounter < ATTACH_CHECK_TICKS) {
return;
}
attachCheckTickCounter = 0;
const bool attached = isAttached();
if (attached == wasAttached) {
pendingAttachConfirmCount = 0;
return;
}
// Require the new state to be confirmed on a second consecutive check before acting -
// a single probe on a floating/half-connected bus (e.g. mid-unplug) can false-positive.
if (attached != pendingAttachState || pendingAttachConfirmCount == 0) {
pendingAttachState = attached;
pendingAttachConfirmCount = 1;
return;
}
pendingAttachConfirmCount = 0;
if (attached) {
reinitDevice();
}
if (!applyAutoRotation(attached)) {
return; // keep prior state so transition is retried
}
wasAttached = attached;
}
// ---------------------------------------------------------------------------
// lateStart - see header comment. Brings up LVGL input handling for a keyboard
// that wasn't attached at boot (startLvgl() wasn't called from attachDevices()).
// ---------------------------------------------------------------------------
bool Tab5Keyboard::lateStart() {
if (kbHandle != nullptr) {
return true; // already started
}
auto* display = lv_display_get_default();
if (display == nullptr) {
return false; // LVGL not ready yet
}
if (!tt::lvgl::lock(pdMS_TO_TICKS(100))) {
return false; // try again on the next attach-state check
}
bool started = startLvgl(display);
if (started) {
tt::lvgl::hardware_keyboard_set_indev(kbHandle);
// redraw() assigns every indev that exists at the time to the active screen's
// input group. This indev didn't exist yet at the last redraw(), so it has no
// group and won't deliver key events until the next app switch. Join the
// current default group now so input works immediately on the visible screen.
lv_indev_set_group(kbHandle, lv_group_get_default());
}
tt::lvgl::unlock();
return started;
}
// ---------------------------------------------------------------------------
// LVGL read callback - called from the LVGL task
// ---------------------------------------------------------------------------
void Tab5Keyboard::readCallback(lv_indev_t* indev, lv_indev_data_t* data) {
auto* self = static_cast<Tab5Keyboard*>(lv_indev_get_user_data(indev));
uint32_t lv_key = 0;
if (xQueueReceive(self->queue, &lv_key, 0) == pdTRUE) {
data->key = lv_key;
data->state = LV_INDEV_STATE_PRESSED;
data->continue_reading = (uxQueueMessagesWaiting(self->queue) > 0);
} else {
data->state = LV_INDEV_STATE_RELEASED;
}
}
// ---------------------------------------------------------------------------
// KeyboardDevice interface
// ---------------------------------------------------------------------------
Tab5Keyboard::~Tab5Keyboard() {
if (inputTimer) {
stopLvgl(); // tears down LVGL indev, IRQ, I2C bus
}
if (queue) {
vQueueDelete(queue);
queue = nullptr;
}
}
// ---------------------------------------------------------------------------
// reinitDevice - (re)applies the device register configuration. Used at
// startLvgl() and again on hot-plug reattach, since the device's RGB mode and
// interrupt configuration are volatile and reset to power-on defaults when
// the keyboard is unplugged and reconnected.
// ---------------------------------------------------------------------------
void Tab5Keyboard::reinitDevice() {
writeReg(REG_KEYBOARD_MODE, 0x00); // Normal mode
writeReg(REG_EVENT_NUM, 0x00); // flush event queue
writeReg(REG_INT_STAT, 0x00); // clear pending INT
writeReg(REG_RGB_MODE, 0x01); // Custom RGB mode (manual LED control)
writeReg(REG_BRIGHTNESS, 50); // 50% brightness
updateLeds(); // restore current LED state
if (irqConfigured) {
writeReg(REG_INT_CFG, 0x01); // re-enable Normal-mode interrupt (bit 0)
}
}
bool Tab5Keyboard::startLvgl(lv_display_t* display) {
if (!queue) {
LOG_E("Tab5Keyboard", "Input queue allocation failed — cannot start");
return false;
}
symActive = false;
aaSticky = false;
aaHeld = false;
aaTapped = false;
ctrlHeld = false;
repeatKey = 0;
repeatRow = 0xFF;
repeatCol = 0xFF;
repeatLastMs = 0;
configureIrqPin(); // best-effort; falls back to polling if it fails. Must run before
// reinitDevice() so REG_INT_CFG is written if IRQ setup succeeded.
// Best-effort: if the keyboard isn't attached yet (e.g. started speculatively at
// boot so it can be detected later via hot-plug), these I2C writes fail silently
// and reinitDevice() runs again once attach is detected.
reinitDevice();
kbHandle = lv_indev_create();
lv_indev_set_type(kbHandle, LV_INDEV_TYPE_KEYPAD);
lv_indev_set_read_cb(kbHandle, readCallback);
lv_indev_set_display(kbHandle, display);
lv_indev_set_user_data(kbHandle, this);
wasAttached = isAttached();
rotationOverrideActive = false;
assert(inputTimer == nullptr);
inputTimer = std::make_unique<tt::Timer>(tt::Timer::Type::Periodic, pdMS_TO_TICKS(20), [this] {
processKeyboard();
});
inputTimer->start();
if (wasAttached) {
applyAutoRotation(true);
}
return true;
}
bool Tab5Keyboard::stopLvgl() {
if (!inputTimer) {
return false; // Not started
}
inputTimer->stop();
inputTimer = nullptr;
removeIrqPin();
if (queue) {
xQueueReset(queue); // discard unread keycodes so a restart begins with an empty buffer
}
writeReg(REG_INT_CFG, 0x00); // disable all interrupts
symActive = false;
aaSticky = false;
aaHeld = false;
updateLeds(); // turn LEDs off
lv_indev_delete(kbHandle);
kbHandle = nullptr;
return true;
}
bool Tab5Keyboard::isAttached() const {
return i2c_controller_has_device_at_address(i2cController, I2C_ADDRESS, pdMS_TO_TICKS(100)) == ERROR_NONE;
}
@@ -1,85 +0,0 @@
#pragma once
#include <Tactility/hal/keyboard/KeyboardDevice.h>
#include <Tactility/Timer.h>
#include <tactility/device.h>
#include <driver/gpio.h>
#include <freertos/queue.h>
class Tab5Keyboard final : public tt::hal::keyboard::KeyboardDevice {
static constexpr uint8_t I2C_ADDRESS = 0x6D;
static constexpr gpio_num_t INT_PIN = GPIO_NUM_50;
// Software key-repeat timing
static constexpr uint32_t REPEAT_INITIAL_MS = 400;
static constexpr uint32_t REPEAT_RATE_MS = 80;
::Device* i2cController = nullptr;
lv_indev_t* kbHandle = nullptr;
QueueHandle_t queue = nullptr;
std::unique_ptr<tt::Timer> inputTimer;
bool symActive = false; // held while Sym is physically down
bool aaSticky = false; // latched on single Aa tap, cleared after next non-modifier key
bool aaHeld = false; // true while Aa is physically held
bool aaTapped = false; // no non-modifier key was pressed while Aa was held
bool ctrlHeld = false; // true while Ctrl is physically held
// IRQ-driven event gating
volatile bool irqPending = false;
bool irqConfigured = false;
// Hot-plug attach-state polling (piggybacks on the 20ms inputTimer)
bool wasAttached = false;
uint32_t attachCheckTickCounter = 0;
// I2C probes can false-positive on a floating/half-connected bus (e.g. mid-unplug), so a
// state change is only acted on once it's seen on two consecutive ~1s checks in a row.
bool pendingAttachState = false;
uint8_t pendingAttachConfirmCount = 0;
lv_display_rotation_t savedRotation = LV_DISPLAY_ROTATION_0;
bool rotationOverrideActive = false;
// Software key-repeat state (tracked by position to survive modifier changes)
uint32_t repeatKey = 0;
uint8_t repeatRow = 0xFF;
uint8_t repeatCol = 0xFF;
uint32_t repeatStartMs = 0;
uint32_t repeatLastMs = 0;
bool readReg(uint8_t reg, uint8_t& value);
bool writeReg(uint8_t reg, uint8_t value);
void updateLeds();
bool configureIrqPin();
void removeIrqPin();
static void IRAM_ATTR irqHandler(void* arg);
void reinitDevice();
bool applyAutoRotation(bool keyboardAttached);
void checkAttachState();
void drainEvents();
void processKeyboard();
static void readCallback(lv_indev_t* indev, lv_indev_data_t* data);
public:
explicit Tab5Keyboard(::Device* i2cController) : i2cController(i2cController) {
queue = xQueueCreate(20, sizeof(uint32_t));
// queue == nullptr on OOM; startLvgl() checks and refuses to start
}
~Tab5Keyboard() override; // defined in .cpp: stops active session, then vQueueDelete(queue)
std::string getName() const override { return "Tab5Keyboard"; }
std::string getDescription() const override { return "M5Stack Tab5 Keyboard addon"; }
bool startLvgl(lv_display_t* display) override;
bool stopLvgl() override;
bool isAttached() const override;
lv_indev_t* getLvglIndev() override { return kbHandle; }
// Starts LVGL input handling and registers the hardware keyboard indev for a device
// that wasn't attached at boot (so startLvgl() was never called from Lvgl.cpp's
// attachDevices()). Called from the device module's attach-detection timer once the
// keyboard is first detected post-boot. No-op if LVGL input is already started.
bool lateStart();
};
@@ -0,0 +1,48 @@
#include "detect.h"
#include <tactility/device.h>
#include <tactility/drivers/i2c_controller.h>
#include <tactility/log.h>
#include <esp_lcd_touch_gt911.h>
#include <esp_lcd_touch_st7123.h>
#include <freertos/FreeRTOS.h>
constexpr auto* TAG = "Tab5";
static Tab5Variant detected_variant = Tab5Variant::Unknown;
Tab5Variant tab5_get_variant() {
return detected_variant;
}
void tab5_set_variant(Tab5Variant variant) {
detected_variant = variant;
}
Tab5Variant tab5_probe_variant(Device* i2c0) {
// Allow time for the touch IC to fully boot after the reset pulse above: 100ms is enough for
// I2C ACK (probe) but cold power-on needs ~300ms before register reads succeed reliably.
vTaskDelay(pdMS_TO_TICKS(300));
constexpr auto PROBE_TIMEOUT = pdMS_TO_TICKS(50);
for (int attempt = 0; attempt < 3; ++attempt) {
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) {
LOG_I(TAG, "display_detect: detected GT911 touch — using variant V1 ");
return Tab5Variant::V1;
}
if (i2c_controller_has_device_at_address(i2c0, ESP_LCD_TOUCH_IO_I2C_ST7123_ADDRESS, PROBE_TIMEOUT) == ERROR_NONE) {
LOG_I(TAG, "display_detect: detected ST7123 touch — using variant V2");
return Tab5Variant::V2;
}
vTaskDelay(pdMS_TO_TICKS(100));
}
LOG_W(TAG, "display_detect: no known touch controller detected, defaulting to V2");
return Tab5Variant::V2;
}
@@ -0,0 +1,37 @@
#pragma once
struct Device;
enum class Tab5Variant {
Unknown,
V1, // Older variant: ILI9881C display + GT911 touch (see devices_v1.cpp)
V2, // Newer variant (default): ST7123 display + in-cell touch (see devices_v2.cpp)
};
// Populated once the device_listener callback in display_detect.cpp has detected which
// panel/touch variant is present (gated on i2c0 + io_expander0 both reaching
// DEVICE_EVENT_STARTED). Safe to call from Configuration.cpp's createDevices()/createTouch():
// kernel_init() (which runs the listener to completion) always finishes before hal::init()
// invokes createDevices().
[[nodiscard]] Tab5Variant tab5_get_variant();
// Registers/unregisters the device_listener (display_detect.cpp) that detects the display/touch
// variant, keyboard and power control devices and dynamically constructs them. Called from
// module.cpp's start()/stop().
void tab5_detect_start();
void tab5_detect_stop();
// --- detect.cpp internals, exposed only for display_detect.cpp's on_display_detect_event() ---
// Records the variant found by probe_variant() below - the only writer of the state
// getDetectedTab5Variant() reads.
void tab5_set_variant(Tab5Variant variant);
// Pulses only the LCD + touch reset bits on io_expander0 (see the pinout table in
// Configuration.cpp). The other io_expander0 pins (speaker enable, RF switch, 5V bus, camera
// reset) are unrelated to display bring-up and stay owned by Configuration.cpp's initExpander0().
bool pulse_display_reset_pins(Device* io_expander0);
// Probes i2c0 for a known touch controller address to determine which display/touch variant is
// present. Must be called after pulse_display_reset_pins().
Tab5Variant tab5_probe_variant(Device* i2c0);
@@ -0,0 +1,45 @@
#include "devices_common.h"
#include <tactility/device.h>
#include <tactility/driver.h>
#include <tactility/log.h>
constexpr auto* TAG = "Tab5";
// Mirrors device_construct_add_start(), but with a device_set_parent() call inserted between
// construct and add - device_set_parent() asserts on device->internal (only valid after
// construct), and device_construct_add_start() doesn't expose a hook to call it before add().
bool construct_add_start(Device* device, Device* parent, const char* compatible) {
error_t error = device_construct(device);
if (error != ERROR_NONE) {
LOG_E(TAG, "display_detect: failed to construct %s: %s", device->name, error_to_string(error));
return false;
}
device_set_parent(device, parent);
Driver* driver = driver_find_compatible(compatible);
if (driver == nullptr) {
LOG_E(TAG, "display_detect: no driver registered for %s", compatible);
device_destruct(device);
return false;
}
device_set_driver(device, driver);
error = device_add(device);
if (error != ERROR_NONE) {
LOG_E(TAG, "display_detect: failed to add %s: %s", device->name, error_to_string(error));
device_destruct(device);
return false;
}
error = device_start(device);
if (error != ERROR_NONE) {
LOG_E(TAG, "display_detect: failed to start %s: %s", device->name, error_to_string(error));
device_remove(device);
device_destruct(device);
return false;
}
return true;
}
@@ -0,0 +1,29 @@
#pragma once
#include <tactility/device.h>
#include <cstddef>
#include <cstdint>
#include <vector>
// Constructs, parents, binds and starts a dynamically-built Device. Mirrors
// device_construct_add_start(), but with a device_set_parent() call inserted between construct and add
bool construct_add_start(Device* device, Device* parent, const char* compatible);
// Unpacks a vendor-typed init-cmd array (ili9881c_lcd_init_cmd_t / st7123_lcd_init_cmd_t - same
// field layout) into the flattened [cmd, data_len, delay_ms, data_len bytes...] byte encoding that
// ili9881c-module/st7123-module's init-sequence config field expects (the same encoding a
// devicetree "array" property would produce - see e.g. ili9881c-module's binding yaml). Needed
// because these Configs are built dynamically here rather than from a .dts node.
template<typename InitCmd>
void flatten_init_sequence(const InitCmd* cmds, size_t count, std::vector<uint8_t>& out) {
for (size_t i = 0; i < count; i++) {
out.push_back(static_cast<uint8_t>(cmds[i].cmd));
out.push_back(static_cast<uint8_t>(cmds[i].data_bytes));
out.push_back(static_cast<uint8_t>(cmds[i].delay_ms));
const auto* data = static_cast<const uint8_t*>(cmds[i].data);
for (size_t j = 0; j < cmds[i].data_bytes; j++) {
out.push_back(data[j]);
}
}
}
@@ -0,0 +1,145 @@
#include "devices_v1.h"
#include "devices_common.h"
#include "ili9881_init_data.h"
#include <tactility/device.h>
#include <tactility/drivers/gpio.h>
#include <tactility/drivers/gpio_controller.h>
#include <tactility/log.h>
#include <drivers/gt911.h>
#include <drivers/ili9881c.h>
#include <iterator>
#include <vector>
constexpr auto* TAG = "Tab5";
// Display Device + Config + flattened init bytes live for the process's whole lifetime (this
// board never tears down or re-detects its panel), so plain static storage is fine - no need to
// heap-allocate something the kernel would have to track.
static std::vector<uint8_t> display_init_bytes;
static Ili9881cConfig ili9881c_config {};
static Device display_device {};
static Gt911Config gt911_config {};
static Device gt911_device {};
// GPIO 23 has a pull-up resistor to 3V3 that blocks GT911 touch, and can't be used as the
// controller's own interrupt pin for that reason (see detect.cpp's probe_variant() comment) -
// driving it low directly sinks that pull-up instead.
// See https://github.com/espressif/esp-bsp/blob/ad668c765cbad177495a122181df0a70ff9f8f61/bsp/m5stack_tab5/src/m5stack_tab5.c#L777
static void apply_gt911_int_workaround() {
Device* gpio0 = nullptr;
if (device_get_by_name("gpio0", &gpio0) != ERROR_NONE) {
LOG_W(TAG, "display_detect: gpio0 not found, GT911 INT workaround not applied");
return;
}
auto* int_pin = gpio_descriptor_acquire(gpio0, 23, GPIO_OWNER_GPIO);
device_put(gpio0);
if (int_pin == nullptr) {
LOG_W(TAG, "display_detect: failed to acquire GPIO23 for GT911 INT workaround");
return;
}
gpio_descriptor_set_flags(int_pin, GPIO_FLAG_DIRECTION_OUTPUT | GPIO_FLAG_PULL_UP);
gpio_descriptor_set_level(int_pin, false);
gpio_descriptor_release(int_pin);
}
// GT911's touch controller is a kernel driver (goodix,gt911, in gt911-module).
static void create_gt911_touch(Device* i2c0) {
gt911_device = Device {
.address = 0,
.name = "touch0",
.config = nullptr,
.parent = nullptr,
.internal = nullptr,
};
gt911_config = Gt911Config {
.address = 0, // unused: the driver auto-probes both known GT911 addresses regardless
.x_max = 720,
.y_max = 1280,
.swap_xy = false,
.mirror_x = false,
.mirror_y = false,
// Reset is pulsed via io_expander0 (detect.cpp's pulse_display_reset_pins), not a direct SoC GPIO.
.pin_reset = GPIO_PIN_SPEC_NONE,
.pin_interrupt = GPIO_PIN_SPEC_NONE,
.reset_active_high = false,
.interrupt_active_high = false,
};
gt911_device.config = &gt911_config;
apply_gt911_int_workaround();
construct_add_start(&gt911_device, i2c0, "goodix,gt911");
}
void tab5_create_devices_v1(Device* i2c0) {
display_device = Device {
.address = 0,
.name = "display0",
.config = nullptr,
.parent = nullptr,
.internal = nullptr,
};
Device* backlight = nullptr;
if (device_get_by_name("display_backlight", &backlight) != ERROR_NONE) {
LOG_W(TAG, "display_detect: display_backlight not found");
}
flatten_init_sequence(ili9881_init_data, std::size(ili9881_init_data), display_init_bytes);
ili9881c_config = Ili9881cConfig {
.horizontal_resolution = 720,
.vertical_resolution = 1280,
.bits_per_pixel = 16,
.bgr_order = false,
.invert_color = false,
.mirror_x = false,
.mirror_y = false,
// LCD reset is pulsed via io_expander0 (detect.cpp's pulse_display_reset_pins), not a
// direct SoC GPIO.
.pin_reset = GPIO_PIN_SPEC_NONE,
.reset_active_high = false,
.ldo_channel = 3,
.ldo_voltage_mv = 2500,
.dsi_bus_id = 0,
.num_data_lanes = 2,
.lane_bit_rate_mbps = 1000,
.dpi_clock_freq_mhz = 60,
.hsync_pulse_width = 40,
.hsync_back_porch = 140,
.hsync_front_porch = 40,
.vsync_pulse_width = 4,
.vsync_back_porch = 20,
.vsync_front_porch = 20,
.num_fbs = 1,
.use_dma2d = false,
.disable_lp = false,
.allow_tearing = true, // matches old lvgl_port_display_dsi_cfg_t.avoid_tearing = 0
.init_sequence = display_init_bytes.data(),
.init_sequence_length = (uint32_t)display_init_bytes.size(),
.backlight = backlight,
};
display_device.config = &ili9881c_config;
if (backlight != nullptr) {
device_put(backlight);
}
Device* root = nullptr;
if (device_get_by_name("/", &root) != ERROR_NONE) {
LOG_E(TAG, "display_detect: root device not found");
return;
}
bool started = construct_add_start(&display_device, root, "ilitek,ili9881c");
device_put(root);
if (!started) {
return;
}
create_gt911_touch(i2c0);
}
@@ -0,0 +1,5 @@
#pragma once
struct Device;
void tab5_create_devices_v1(Device* i2c0);
@@ -0,0 +1,124 @@
#include "devices_v2.h"
#include "devices_common.h"
#include "st7123_init_data.h"
#include <tactility/device.h>
#include <tactility/drivers/gpio.h>
#include <tactility/log.h>
#include <drivers/st7123.h>
#include <drivers/st7123_touch.h>
#include <iterator>
#include <vector>
#define TAG "Tab5"
static std::vector<uint8_t> display_init_bytes;
static St7123Config st7123_config {};
static Device display_device {};
static St7123TouchConfig st7123_touch_config {};
static Device st7123_touch_device {};
static void create_st7123_touch(Device* i2c0) {
st7123_touch_device = Device {
.address = 0,
.name = "touch0",
.config = nullptr,
.parent = nullptr,
.internal = nullptr,
};
GpioPinSpec pin_interrupt = GPIO_PIN_SPEC_NONE;
Device* gpio0 = nullptr;
if (device_get_by_name("gpio0", &gpio0) == ERROR_NONE) {
pin_interrupt = GpioPinSpec { gpio0, 23, GPIO_FLAG_NONE };
device_put(gpio0);
} else {
LOG_W(TAG, "display_detect: gpio0 not found, touch interrupt pin will not be wired");
}
st7123_touch_config = St7123TouchConfig {
.address = 0x55, // fixed - see ESP_LCD_TOUCH_IO_I2C_ST7123_ADDRESS
.x_max = 720,
.y_max = 1280,
.swap_xy = false,
.mirror_x = false,
.mirror_y = false,
// Reset is pulsed via io_expander0 (detect.cpp's pulse_display_reset_pins), not a direct SoC GPIO.
.pin_reset = GPIO_PIN_SPEC_NONE,
.pin_interrupt = pin_interrupt,
.reset_active_high = false,
.interrupt_active_high = false,
};
st7123_touch_device.config = &st7123_touch_config;
construct_add_start(&st7123_touch_device, i2c0, "sitronix,st7123-touch");
}
void tab5_create_devices_v2(Device* i2c0) {
display_device = Device {
.address = 0,
.name = "display0",
.config = nullptr,
.parent = nullptr,
.internal = nullptr,
};
Device* backlight = nullptr;
if (device_get_by_name("display_backlight", &backlight) != ERROR_NONE) {
LOG_W(TAG, "display_detect: display_backlight not found");
}
flatten_init_sequence(st7123_init_data, std::size(st7123_init_data), display_init_bytes);
st7123_config = St7123Config {
.horizontal_resolution = 720,
.vertical_resolution = 1280,
.bits_per_pixel = 16,
.bgr_order = false,
.invert_color = false,
.mirror_x = false,
.mirror_y = false,
.pin_reset = GPIO_PIN_SPEC_NONE,
.reset_active_high = false,
.ldo_channel = 3,
.ldo_voltage_mv = 2500,
.dsi_bus_id = 0,
.num_data_lanes = 2,
.lane_bit_rate_mbps = 965, // ST7123 lane bitrate per M5Stack BSP
.dpi_clock_freq_mhz = 70,
.hsync_pulse_width = 2,
.hsync_back_porch = 40,
.hsync_front_porch = 40,
.vsync_pulse_width = 2,
.vsync_back_porch = 8,
.vsync_front_porch = 220,
.num_fbs = 1,
.use_dma2d = false,
.disable_lp = false,
.allow_tearing = true,
.init_sequence = display_init_bytes.data(),
.init_sequence_length = (uint32_t)display_init_bytes.size(),
.backlight = backlight,
};
display_device.config = &st7123_config;
if (backlight != nullptr) {
device_put(backlight);
}
Device* root = nullptr;
if (device_get_by_name("/", &root) != ERROR_NONE) {
LOG_E(TAG, "display_detect: root device not found");
return;
}
bool started = construct_add_start(&display_device, root, "sitronix,st7123");
device_put(root);
if (!started) {
return;
}
create_st7123_touch(i2c0);
}
@@ -0,0 +1,5 @@
#pragma once
struct Device;
void tab5_create_devices_v2(Device* i2c0);
@@ -0,0 +1,138 @@
#include "detect.h"
#include "../../../../TactilityKernel/include/tactility/log.h"
#include "devices_common.h"
#include "devices_v1.h"
#include "devices_v2.h"
#include "tab5_keyboard.h"
#include <tactility/device.h>
#include <tactility/device_listener.h>
#include <tactility/drivers/gpio_controller.h>
#include <tactility/log.h>
#include <cstring>
constexpr auto* TAG = "Tab5";
static Device tab5_power_control_device {};
// Charge-control/quick-charge/power-off for the IP2326 charger IC, driven through io_expander1 (see tab5_power_control.h).
static void tab5_create_power_control(Device* io_expander1) {
tab5_power_control_device = Device {
.address = 0,
.name = "power_control0",
.config = nullptr,
.parent = nullptr,
.internal = nullptr,
};
// Parented to io_expander1 itself: the driver's start()/set_*() use device_get_parent() as
// its GPIO controller.
construct_add_start(&tab5_power_control_device, io_expander1, "m5stack,tab5-power-control");
}
constexpr auto GPIO_EXP0_PIN_LCD_RESET = 4;
constexpr auto GPIO_EXP0_PIN_TOUCH_RESET = 5;
bool pulse_display_reset_pins(Device* io_expander0) {
auto* lcd_reset_pin = gpio_descriptor_acquire(io_expander0, GPIO_EXP0_PIN_LCD_RESET, GPIO_OWNER_GPIO);
auto* touch_reset_pin = gpio_descriptor_acquire(io_expander0, GPIO_EXP0_PIN_TOUCH_RESET, GPIO_OWNER_GPIO);
if (lcd_reset_pin == nullptr || touch_reset_pin == nullptr) {
LOG_E(TAG, "display_detect: failed to acquire LCD/touch reset pins on io_expander0");
if (lcd_reset_pin != nullptr) {
gpio_descriptor_release(lcd_reset_pin);
}
if (touch_reset_pin != nullptr) {
gpio_descriptor_release(touch_reset_pin);
}
return false;
}
gpio_descriptor_set_flags(lcd_reset_pin, GPIO_FLAG_DIRECTION_OUTPUT);
gpio_descriptor_set_flags(touch_reset_pin, GPIO_FLAG_DIRECTION_OUTPUT);
gpio_descriptor_set_level(lcd_reset_pin, false);
gpio_descriptor_set_level(touch_reset_pin, false);
vTaskDelay(pdMS_TO_TICKS(10));
gpio_descriptor_set_level(lcd_reset_pin, true);
gpio_descriptor_set_level(touch_reset_pin, true);
gpio_descriptor_release(lcd_reset_pin);
gpio_descriptor_release(touch_reset_pin);
return true;
}
// Fires for every device's start/stop in the system.
static void on_display_detect_event(Device* device, DeviceEvent event, void* context) {
(void)context;
static Device* i2c0 = nullptr;
static Device* io_expander0 = nullptr;
static bool did_create_display = false;
static Device* i2c2 = nullptr;
static bool did_create_keyboard = false;
static Device* io_expander1 = nullptr;
static bool did_create_power_control = false;
if (event == DEVICE_EVENT_STARTED) {
if (strcmp(device->name, "i2c0") == 0) {
i2c0 = device;
}
if (strcmp(device->name, "io_expander0") == 0) {
io_expander0 = device;
}
if (strcmp(device->name, "i2c2") == 0) {
i2c2 = device;
}
if (strcmp(device->name, "io_expander1") == 0) {
io_expander1 = device;
}
}
if (io_expander0 != nullptr && tab5_get_variant() == Tab5Variant::Unknown) {
Tab5Variant variant = tab5_probe_variant(i2c0);
tab5_set_variant(variant);
}
// We need i2c0 and io_expander0 to create the display and touch devices
if (!did_create_display && i2c0 != nullptr && io_expander0 != nullptr) {
did_create_display = true;
if (pulse_display_reset_pins(io_expander0)) {
switch (tab5_get_variant()) {
case Tab5Variant::Unknown:
LOG_E(TAG, "Variant not detected yet");
break;
case Tab5Variant::V1:
tab5_create_devices_v1(i2c0);
break;
case Tab5Variant::V2:
tab5_create_devices_v2(i2c0);
break;
}
} else {
LOG_E(TAG, "display_detect: skipping display creation, failed to pulse reset pins");
}
}
// The keyboard lives on the unrelated i2c2 bus, so this is gated independently.
if (!did_create_keyboard && i2c2 != nullptr) {
did_create_keyboard = true;
tab5_create_keyboard(i2c2);
}
// The charger control lines live on io_expander1, unrelated to the display/keyboard buses.
if (!did_create_power_control && io_expander1 != nullptr) {
did_create_power_control = true;
tab5_create_power_control(io_expander1);
}
}
void tab5_detect_start() {
device_listener_add(on_display_detect_event, nullptr);
}
void tab5_detect_stop() {
device_listener_remove(on_display_detect_event);
}
@@ -0,0 +1,109 @@
#include "tab5_headphone_detect.h"
#include <tactility/device.h>
#include <tactility/drivers/gpio_controller.h>
#include <tactility/log.h>
#include <freertos/FreeRTOS.h>
#include <freertos/timers.h>
#include <atomic>
#define TAG "Tab5"
// PI4IOE5V6408-0 (0x43) bit 1
constexpr auto GPIO_EXP0_PIN_SPEAKER_ENABLE = 1;
// PI4IOE5V6408-0 (0x43) bit 7
constexpr auto GPIO_EXP0_PIN_HEADPHONE_DETECT = 7;
constexpr auto HP_DETECT_POLL_MS = 1000;
static TimerHandle_t hp_detect_timer = nullptr;
static std::atomic<Device*> io_expander0_cached { nullptr };
// Flags are written by the timer daemon task
static std::atomic hp_detect_last { false };
static std::atomic hp_detect_initialized { false };
static void headphone_detect_callback(TimerHandle_t /*timer*/) {
Device* cached = io_expander0_cached.load(std::memory_order_acquire);
if (!cached) {
cached = device_find_by_name("io_expander0");
io_expander0_cached.store(cached, std::memory_order_release);
}
auto* io_expander0 = cached;
if (!io_expander0) {
return; // Not ready yet, will retry on next tick
}
auto* hp_pin = gpio_descriptor_acquire(io_expander0, GPIO_EXP0_PIN_HEADPHONE_DETECT, GPIO_OWNER_GPIO);
if (!hp_pin) {
LOG_W(TAG, "hp_detect: HP_DET pin busy");
return;
}
bool hp = false;
error_t err = gpio_descriptor_get_level(hp_pin, &hp);
gpio_descriptor_release(hp_pin);
if (err != ERROR_NONE) {
LOG_W(TAG, "hp_detect: HP_DET read error: %s", error_to_string(err));
return;
}
LOG_D(TAG, "hp_detect: HP_DET=%d", (int)hp);
if (!hp_detect_initialized || hp != hp_detect_last) {
auto* spk_pin = gpio_descriptor_acquire(io_expander0, GPIO_EXP0_PIN_SPEAKER_ENABLE, GPIO_OWNER_GPIO);
if (!spk_pin) {
LOG_W(TAG, "hp_detect: SPK_EN pin busy, will retry");
return;
}
error_t spk_err = gpio_descriptor_set_level(spk_pin, !hp);
gpio_descriptor_release(spk_pin);
if (spk_err != ERROR_NONE) {
LOG_W(TAG, "hp_detect: SPK_EN set error: %s, will retry", error_to_string(spk_err));
return;
}
hp_detect_last = hp;
hp_detect_initialized = true;
LOG_I(TAG, "Headphones %s, speaker %s", hp ? "detected" : "removed", hp ? "disabled" : "enabled");
}
}
void tab5_headphone_detect_start() {
if (hp_detect_timer != nullptr) {
LOG_W(TAG, "hp_detect timer already running");
return;
}
hp_detect_initialized = false;
hp_detect_last = false;
hp_detect_timer = xTimerCreate("hp_detect", pdMS_TO_TICKS(HP_DETECT_POLL_MS), pdTRUE, nullptr, headphone_detect_callback);
if (!hp_detect_timer) {
LOG_E(TAG, "Failed to create hp_detect timer");
return;
}
if (xTimerStart(hp_detect_timer, pdMS_TO_TICKS(100)) != pdPASS) {
LOG_E(TAG, "Failed to start hp_detect timer");
xTimerDelete(hp_detect_timer, pdMS_TO_TICKS(100));
hp_detect_timer = nullptr;
}
}
void tab5_headphone_detect_stop() {
if (hp_detect_timer == nullptr) {
return;
}
if (xTimerStop(hp_detect_timer, pdMS_TO_TICKS(100)) != pdPASS) {
LOG_W(TAG, "Failed to stop hp_detect timer");
}
if (xTimerDelete(hp_detect_timer, pdMS_TO_TICKS(100)) != pdPASS) {
LOG_E(TAG, "Failed to delete hp_detect timer");
}
// Always clear the handle — stale non-null handle is worse than a resource leak, as it would
// cause tab5_headphone_detect_start() to silently skip re-creating the timer.
hp_detect_timer = nullptr;
io_expander0_cached.store(nullptr, std::memory_order_release);
}
@@ -0,0 +1,6 @@
#pragma once
// Starts/stops the periodic headphone-jack detection poll (HP_DET pin on io_expander0), which
// toggles the speaker amplifier enable pin accordingly. Called from module.cpp's start()/stop().
void tab5_headphone_detect_start();
void tab5_headphone_detect_stop();
@@ -0,0 +1,695 @@
// SPDX-License-Identifier: Apache-2.0
#include "tab5_keyboard.h"
#include "devices_common.h"
#include <tactility/check.h>
#include <tactility/device.h>
#include <tactility/driver.h>
#include <tactility/drivers/i2c_controller.h>
#include <tactility/drivers/keyboard.h>
#include <tactility/log.h>
#include <tactility/lvgl_module.h>
#include <tactility/module.h>
#include <driver/gpio.h>
#include <esp_timer.h>
#include <freertos/FreeRTOS.h>
#include <freertos/queue.h>
#include <lvgl.h>
#include <cstdlib>
#include <cstring>
constexpr auto* TAG = "Tab5Keyboard";
#define GET_CONFIG(device) (static_cast<const Tab5KeyboardConfig*>((device)->config))
static constexpr uint8_t I2C_ADDRESS = 0x6D;
// Software key-repeat timing
static constexpr uint32_t REPEAT_INITIAL_MS = 400;
static constexpr uint32_t REPEAT_RATE_MS = 80;
// I2C event-poll interval - mirrors the old deprecated-HAL's 20ms Timer period. Drives both
// REG_INT_STAT polling (when no IRQ pin) and software key-repeat ticking.
static constexpr uint32_t POLL_INTERVAL_MS = 20;
// Hot-plug attach-state check interval. I2C probes can false-positive on a floating/half-connected
// bus (e.g. mid-unplug), so a state change is only acted on once it's seen on two consecutive
// checks in a row (see check_attach_state()).
static constexpr uint32_t ATTACH_CHECK_INTERVAL_MS = 1000;
// ---------------------------------------------------------------------------
// Register addresses
// ---------------------------------------------------------------------------
static constexpr uint8_t REG_INT_CFG = 0x00;
static constexpr uint8_t REG_INT_STAT = 0x01;
static constexpr uint8_t REG_EVENT_NUM = 0x02;
static constexpr uint8_t REG_BRIGHTNESS = 0x03;
static constexpr uint8_t REG_KEYBOARD_MODE = 0x10;
static constexpr uint8_t REG_RGB_MODE = 0x11;
static constexpr uint8_t REG_KEY_EVENT = 0x20;
static constexpr uint8_t REG_RGB_BASE = 0x60;
static constexpr uint8_t KEY_EVENT_EMPTY = 0xFF;
// ---------------------------------------------------------------------------
// Modifier key positions in the 5x14 matrix
// ---------------------------------------------------------------------------
static constexpr uint8_t MOD_ROW_SYM = 3, MOD_COL_SYM = 0;
static constexpr uint8_t MOD_ROW_AA = 3, MOD_COL_AA = 1;
static constexpr uint8_t MOD_ROW_CTRL = 4, MOD_COL_CTRL = 0;
static constexpr uint8_t MOD_ROW_ALT = 4, MOD_COL_ALT = 1;
// ---------------------------------------------------------------------------
// HID lookup tables
// Row-major: index = row * 14 + col, 5 rows x 14 cols = 70 entries.
// modifier 0x02 = Left Shift (pre-baked by firmware for shifted characters).
// ---------------------------------------------------------------------------
struct HidMapping {
uint8_t keycode;
uint8_t modifier;
};
static constexpr HidMapping KEY_MATRIX_HID_BASE[70] = {
// Row 0: Esc 1 2 3 4 5 6 7 8 9 0 - + Del
{0x29, 0x00}, {0x1E, 0x00}, {0x1F, 0x00}, {0x20, 0x00}, {0x21, 0x00}, {0x22, 0x00},
{0x23, 0x00}, {0x24, 0x00}, {0x25, 0x00}, {0x26, 0x00}, {0x27, 0x00}, {0x2D, 0x00},
{0x2E, 0x02}, {0x4C, 0x00},
// Row 1: ` ! @ # $ % ^ & * ( ) [ ] backslash
{0x35, 0x00}, {0x1E, 0x02}, {0x1F, 0x02}, {0x20, 0x02}, {0x21, 0x02}, {0x22, 0x02},
{0x23, 0x02}, {0x24, 0x02}, {0x25, 0x02}, {0x26, 0x02}, {0x27, 0x02}, {0x2F, 0x00},
{0x30, 0x00}, {0x31, 0x00},
// Row 2: Tab q w e r t y u i o p ; ' Backspace
{0x2B, 0x00}, {0x14, 0x00}, {0x1A, 0x00}, {0x08, 0x00}, {0x15, 0x00}, {0x17, 0x00},
{0x1C, 0x00}, {0x18, 0x00}, {0x0C, 0x00}, {0x12, 0x00}, {0x13, 0x00}, {0x33, 0x00},
{0x34, 0x00}, {0x2A, 0x00},
// Row 3: Sym Aa a s d f g h j k l ↑ _ Enter
{0x00, 0x00}, {0x00, 0x00}, {0x04, 0x00}, {0x16, 0x00}, {0x07, 0x00}, {0x09, 0x00},
{0x0A, 0x00}, {0x0B, 0x00}, {0x0D, 0x00}, {0x0E, 0x00}, {0x0F, 0x00}, {0x52, 0x00},
{0x2D, 0x02}, {0x28, 0x00},
// Row 4: Ctrl Alt z x c v b n m . ← ↓ → Space
{0x00, 0x00}, {0x00, 0x00}, {0x1D, 0x00}, {0x1B, 0x00}, {0x06, 0x00}, {0x19, 0x00},
{0x05, 0x00}, {0x11, 0x00}, {0x10, 0x00}, {0x37, 0x00}, {0x50, 0x00}, {0x51, 0x00},
{0x4F, 0x00}, {0x2C, 0x00},
};
static constexpr HidMapping KEY_MATRIX_HID_SYM[70] = {
// Row 0: identical to base
{0x29, 0x00}, {0x1E, 0x00}, {0x1F, 0x00}, {0x20, 0x00}, {0x21, 0x00}, {0x22, 0x00},
{0x23, 0x00}, {0x24, 0x00}, {0x25, 0x00}, {0x26, 0x00}, {0x27, 0x00}, {0x2D, 0x00},
{0x2E, 0x02}, {0x4C, 0x00},
// Row 1: Sym deltas: ` → ~, ! → ?, * → /, ( → <, ) → >, [ → {, ] → }, backslash → |
{0x35, 0x02}, {0x38, 0x02}, {0x1F, 0x02}, {0x20, 0x02}, {0x21, 0x02}, {0x22, 0x02},
{0x23, 0x02}, {0x24, 0x02}, {0x38, 0x00}, {0x36, 0x02}, {0x37, 0x02}, {0x2F, 0x02},
{0x30, 0x02}, {0x31, 0x02},
// Row 2: Sym deltas: ; → :, ' → "
{0x2B, 0x00}, {0x14, 0x00}, {0x1A, 0x00}, {0x08, 0x00}, {0x15, 0x00}, {0x17, 0x00},
{0x1C, 0x00}, {0x18, 0x00}, {0x0C, 0x00}, {0x12, 0x00}, {0x13, 0x00}, {0x33, 0x02},
{0x34, 0x02}, {0x2A, 0x00},
// Row 3: Sym delta: _ → =
{0x00, 0x00}, {0x00, 0x00}, {0x04, 0x00}, {0x16, 0x00}, {0x07, 0x00}, {0x09, 0x00},
{0x0A, 0x00}, {0x0B, 0x00}, {0x0D, 0x00}, {0x0E, 0x00}, {0x0F, 0x00}, {0x52, 0x00},
{0x2E, 0x00}, {0x28, 0x00},
// Row 4: Sym delta: . → ,
{0x00, 0x00}, {0x00, 0x00}, {0x1D, 0x00}, {0x1B, 0x00}, {0x06, 0x00}, {0x19, 0x00},
{0x05, 0x00}, {0x11, 0x00}, {0x10, 0x00}, {0x36, 0x00}, {0x50, 0x00}, {0x51, 0x00},
{0x4F, 0x00}, {0x2C, 0x00},
};
// ---------------------------------------------------------------------------
// HID usage code + modifier → LVGL key
// Covers all codes present in the Tab5 matrix tables above. LV_KEY_* are plain uint32_t
// constants - matching KeyboardKeyData::key's driver-defined contract and the same convention
// m5stack-module's cardputer_keyboard.cpp kernel driver already uses.
// ---------------------------------------------------------------------------
static uint32_t tab5_translate_key(uint8_t keycode, uint8_t modifier, bool ctrl) {
const bool shift = (modifier & 0x22U) != 0U;
// Navigation → LVGL key constants
switch (keycode) {
case 0x29: return LV_KEY_ESC;
case 0x28: return LV_KEY_ENTER;
case 0x2A: return LV_KEY_BACKSPACE;
case 0x4C: return LV_KEY_DEL;
case 0x2B: return '\t';
// Arrows: Ctrl+arrow = focus navigation, plain arrow = raw cursor movement
case 0x52: return ctrl ? (uint32_t)LV_KEY_PREV : (uint32_t)LV_KEY_UP;
case 0x51: return ctrl ? (uint32_t)LV_KEY_NEXT : (uint32_t)LV_KEY_DOWN;
case 0x50: return ctrl ? (uint32_t)LV_KEY_PREV : (uint32_t)LV_KEY_LEFT;
case 0x4F: return ctrl ? (uint32_t)LV_KEY_NEXT : (uint32_t)LV_KEY_RIGHT;
default: break;
}
// Letters az / AZ
if (keycode >= 0x04U && keycode <= 0x1DU) {
uint32_t c = static_cast<uint32_t>('a' + (keycode - 0x04U));
return shift ? (c - 0x20U) : c;
}
// Numbers 10 and their shifted symbols
if (keycode >= 0x1EU && keycode <= 0x27U) {
static constexpr char nums[] = "1234567890";
static constexpr char snums[] = "!@#$%^&*()";
return shift ? static_cast<uint32_t>(snums[keycode - 0x1EU])
: static_cast<uint32_t>(nums[keycode - 0x1EU]);
}
// Space and punctuation - all codes present in the Tab5 matrix
switch (keycode) {
case 0x2C: return ' ';
case 0x2D: return shift ? '_' : '-';
case 0x2E: return shift ? '+' : '=';
case 0x2F: return shift ? '{' : '[';
case 0x30: return shift ? '}' : ']';
case 0x31: return shift ? '|' : '\\';
case 0x33: return shift ? ':' : ';';
case 0x34: return shift ? '"' : '\'';
case 0x35: return shift ? '~' : '`';
case 0x36: return shift ? '<' : ',';
case 0x37: return shift ? '>' : '.';
case 0x38: return shift ? '?' : '/';
default: return 0;
}
}
static uint32_t now_ms() {
return static_cast<uint32_t>(esp_timer_get_time() / 1000);
}
struct Tab5KeyboardInternal {
QueueHandle_t queue;
// Modifier state
bool sym_active;
bool aa_sticky;
bool aa_held;
bool aa_tapped;
bool ctrl_held;
// IRQ-driven event gating
volatile bool irq_pending;
bool irq_configured;
gpio_num_t irq_pin;
// Poll/attach-check throttling (real-time based, since read_key() is called at whatever rate
// LVGL's indev timer and its own drain-loop - via continue_reading - happen to run at, unlike
// the old deprecated-HAL's fixed 20ms Timer)
uint32_t last_poll_ms;
uint32_t last_attach_check_ms;
bool was_attached;
bool pending_attach_state;
uint8_t pending_attach_confirm_count;
// Software key-repeat state (tracked by position to survive modifier changes)
uint32_t repeat_key;
uint8_t repeat_row;
uint8_t repeat_col;
uint32_t repeat_start_ms;
uint32_t repeat_last_ms;
Tab5KeyboardAttachListener attach_listener;
void* attach_listener_context;
};
// ---------------------------------------------------------------------------
// I2C helpers
// ---------------------------------------------------------------------------
static bool read_reg(Device* device, uint8_t reg, uint8_t* value) {
auto* parent = device_get_parent(device);
return i2c_controller_read_register(parent, I2C_ADDRESS, reg, value, 1, pdMS_TO_TICKS(50)) == ERROR_NONE;
}
static bool write_reg(Device* device, uint8_t reg, uint8_t value) {
auto* parent = device_get_parent(device);
return i2c_controller_write_register(parent, I2C_ADDRESS, reg, &value, 1, pdMS_TO_TICKS(50)) == ERROR_NONE;
}
static bool is_attached_raw(Device* device) {
auto* parent = device_get_parent(device);
return i2c_controller_has_device_at_address(parent, I2C_ADDRESS, pdMS_TO_TICKS(100)) == ERROR_NONE;
}
// ---------------------------------------------------------------------------
// LED helpers - LED0 = Sym indicator (green), LED1 = Aa indicator (red)
// RGB register layout: [B, G, R] per LED, stride 4 (byte 3 reserved)
// ---------------------------------------------------------------------------
static void update_leds(Device* device, const Tab5KeyboardInternal* internal) {
auto* parent = device_get_parent(device);
// [LED0: B,G,R, reserved, LED1: B,G,R]
uint8_t buf[7] = {
0x00, internal->sym_active ? uint8_t(0xA0) : uint8_t(0x00), 0x00, 0x00,
0x00, 0x00, internal->aa_sticky ? uint8_t(0xA0) : uint8_t(0x00),
};
i2c_controller_write_register(parent, I2C_ADDRESS, REG_RGB_BASE, buf, 7, pdMS_TO_TICKS(50));
}
// ---------------------------------------------------------------------------
// IRQ pin - active-low, falling edge
// ---------------------------------------------------------------------------
static void IRAM_ATTR irq_handler(void* arg) {
auto* internal = static_cast<Tab5KeyboardInternal*>(arg);
internal->irq_pending = true;
}
static bool configure_irq_pin(Tab5KeyboardInternal* internal) {
gpio_config_t io_conf{};
io_conf.pin_bit_mask = (1ULL << internal->irq_pin);
io_conf.mode = GPIO_MODE_INPUT;
io_conf.pull_up_en = GPIO_PULLUP_ENABLE;
io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE;
io_conf.intr_type = GPIO_INTR_NEGEDGE;
if (gpio_config(&io_conf) != ESP_OK) {
return false;
}
const esp_err_t svc = gpio_install_isr_service(0);
if (svc != ESP_OK && svc != ESP_ERR_INVALID_STATE) {
return false;
}
if (gpio_isr_handler_add(internal->irq_pin, irq_handler, internal) != ESP_OK) {
gpio_set_intr_type(internal->irq_pin, GPIO_INTR_DISABLE);
return false;
}
internal->irq_configured = true;
return true;
}
static void remove_irq_pin(Tab5KeyboardInternal* internal) {
if (!internal->irq_configured) {
return;
}
gpio_isr_handler_remove(internal->irq_pin);
internal->irq_configured = false;
internal->irq_pending = false;
}
// ---------------------------------------------------------------------------
// drain_events - reads all pending events from the device queue
// ---------------------------------------------------------------------------
static void drain_events(Device* device, Tab5KeyboardInternal* internal) {
uint8_t count = 0;
if (!read_reg(device, REG_EVENT_NUM, &count) || count == 0) {
return;
}
while (count > 0) {
uint8_t raw = 0;
if (!read_reg(device, REG_KEY_EVENT, &raw) || raw == KEY_EVENT_EMPTY) {
break;
}
const bool pressed = (raw & 0x80U) != 0U;
const uint8_t row = (raw >> 4U) & 0x07U;
const uint8_t col = raw & 0x0FU;
// Modifier keys: update state, no key output
if (row == MOD_ROW_SYM && col == MOD_COL_SYM) {
internal->sym_active = pressed;
update_leds(device, internal);
count--;
continue;
}
if (row == MOD_ROW_AA && col == MOD_COL_AA) {
if (pressed) {
internal->aa_held = true;
internal->aa_tapped = true; // assume tap until a real key is pressed while held
} else {
// Only latch sticky if no non-modifier key was pressed during this hold
if (internal->aa_tapped) {
internal->aa_sticky = !internal->aa_sticky;
}
internal->aa_held = false;
internal->aa_tapped = false;
}
update_leds(device, internal);
count--;
continue;
}
if (row == MOD_ROW_CTRL && col == MOD_COL_CTRL) {
internal->ctrl_held = pressed;
count--;
continue;
}
if (row == MOD_ROW_ALT && col == MOD_COL_ALT) {
count--;
continue;
}
if (row < 5U && col < 14U) {
const bool aa_active = internal->aa_held || internal->aa_sticky;
const HidMapping& m = internal->sym_active
? KEY_MATRIX_HID_SYM[row * 14U + col]
: KEY_MATRIX_HID_BASE[row * 14U + col];
if (m.keycode != 0U) {
const uint8_t modifier = static_cast<uint8_t>(m.modifier | (aa_active ? 0x02U : 0U));
const uint32_t lv_key = tab5_translate_key(m.keycode, modifier, internal->ctrl_held);
if (lv_key != 0U) {
if (pressed) {
// A real key was pressed — this hold is a chord, not a tap
internal->aa_tapped = false;
// Note: ESC used to stop the foreground app directly (tt::app::stop()) in
// the deprecated-HAL version - that's an app-layer concern the driver has
// no business reaching into, so ESC is now just queued as a normal key
// like everything else (LVGL/app code already handles ESC via focus/group
// navigation the same way a dedicated ESC key on any other keyboard would).
xQueueSend(internal->queue, &lv_key, 0);
// Arm software repeat tracking by row/col to survive modifier changes
const uint32_t now = now_ms();
internal->repeat_key = lv_key;
internal->repeat_row = row;
internal->repeat_col = col;
internal->repeat_start_ms = now;
internal->repeat_last_ms = 0;
// Consume sticky Aa after one keypress
if (internal->aa_sticky) {
internal->aa_sticky = false;
internal->aa_held = false;
update_leds(device, internal);
}
} else if (row == internal->repeat_row && col == internal->repeat_col) {
// Match release by position, not translated value — survives sticky Aa clear
internal->repeat_key = 0;
}
}
}
}
count--;
}
// Clear INT status after draining so the line de-asserts
write_reg(device, REG_INT_STAT, 0x00);
}
// ---------------------------------------------------------------------------
// reinit_device - (re)applies the device register configuration. Used at start() and again on
// hot-plug reattach, since the device's RGB mode and interrupt configuration are volatile and
// reset to power-on defaults when the keyboard is unplugged and reconnected.
// ---------------------------------------------------------------------------
static void reinit_device(Device* device, Tab5KeyboardInternal* internal) {
write_reg(device, REG_KEYBOARD_MODE, 0x00); // Normal mode
write_reg(device, REG_EVENT_NUM, 0x00); // flush event queue
write_reg(device, REG_INT_STAT, 0x00); // clear pending INT
write_reg(device, REG_RGB_MODE, 0x01); // Custom RGB mode (manual LED control)
write_reg(device, REG_BRIGHTNESS, 50); // 50% brightness
update_leds(device, internal); // restore current LED state
if (internal->irq_configured) {
write_reg(device, REG_INT_CFG, 0x01); // re-enable Normal-mode interrupt (bit 0)
}
}
// ---------------------------------------------------------------------------
// check_attach_state - throttled (~1s) hot-plug detection. Reapplies device register
// configuration on reattach, and notifies the registered attach listener (if any) of confirmed
// transitions - see Tab5KeyboardAttachListener's doc comment for the retry contract.
// ---------------------------------------------------------------------------
static void check_attach_state(Device* device, Tab5KeyboardInternal* internal) {
uint32_t now = now_ms();
if (now - internal->last_attach_check_ms < ATTACH_CHECK_INTERVAL_MS) {
return;
}
internal->last_attach_check_ms = now;
const bool attached = is_attached_raw(device);
if (attached == internal->was_attached) {
internal->pending_attach_confirm_count = 0;
return;
}
// Require the new state to be confirmed on a second consecutive check before acting - a
// single probe on a floating/half-connected bus (e.g. mid-unplug) can false-positive.
if (attached != internal->pending_attach_state || internal->pending_attach_confirm_count == 0) {
internal->pending_attach_state = attached;
internal->pending_attach_confirm_count = 1;
return;
}
internal->pending_attach_confirm_count = 0;
if (attached) {
reinit_device(device, internal);
}
if (internal->attach_listener != nullptr) {
if (!internal->attach_listener(device, attached, internal->attach_listener_context)) {
return; // not handled yet (e.g. LVGL lock busy) - retry on the next confirmed check
}
}
internal->was_attached = attached;
}
// ---------------------------------------------------------------------------
// poll_if_due - the closest equivalent to the old deprecated-HAL's 20ms-Timer-driven
// processKeyboard(): drains new key events (IRQ-gated or polled), ticks software key-repeat, and
// checks hot-plug attach state. Called from read_key(), throttled to real elapsed time rather
// than call count, since read_key() can be called back-to-back multiple times per LVGL indev
// timer tick while draining an already-queued burst (continue_reading).
// ---------------------------------------------------------------------------
static void poll_if_due(Device* device, Tab5KeyboardInternal* internal) {
uint32_t now = now_ms();
if (!internal->irq_pending && (now - internal->last_poll_ms) < POLL_INTERVAL_MS) {
return;
}
internal->last_poll_ms = now;
bool should_drain = false;
if (internal->irq_configured) {
if (internal->irq_pending) {
internal->irq_pending = false;
should_drain = true;
}
} else {
// Polling: check INT_STAT first — bit 0 = Normal mode event pending
uint8_t status = 0;
if (read_reg(device, REG_INT_STAT, &status) && (status & 0x01U)) {
should_drain = true;
}
}
if (should_drain) {
drain_events(device, internal);
}
// Software key-repeat (runs every tick regardless of IRQ)
if (internal->repeat_key != 0U) {
if ((now - internal->repeat_start_ms) >= REPEAT_INITIAL_MS) {
const uint32_t last = internal->repeat_last_ms;
if (last == 0 || (now - last) >= REPEAT_RATE_MS) {
internal->repeat_last_ms = now;
xQueueSend(internal->queue, &internal->repeat_key, 0);
}
}
}
check_attach_state(device, internal);
}
static gpio_num_t pin_or_nc(const GpioPinSpec& pin) {
return pin.gpio_controller == nullptr ? GPIO_NUM_NC : static_cast<gpio_num_t>(pin.pin);
}
// region Driver lifecycle
static error_t start(Device* device) {
auto* parent = device_get_parent(device);
check(device_get_type(parent) == &I2C_CONTROLLER_TYPE);
const auto* config = GET_CONFIG(device);
auto* internal = static_cast<Tab5KeyboardInternal*>(malloc(sizeof(Tab5KeyboardInternal)));
if (internal == nullptr) {
return ERROR_OUT_OF_MEMORY;
}
memset(internal, 0, sizeof(Tab5KeyboardInternal));
internal->queue = xQueueCreate(20, sizeof(uint32_t));
if (internal->queue == nullptr) {
free(internal);
return ERROR_OUT_OF_MEMORY;
}
internal->repeat_row = 0xFF;
internal->repeat_col = 0xFF;
internal->irq_pin = pin_or_nc(config->pin_interrupt);
if (internal->irq_pin != GPIO_NUM_NC) {
configure_irq_pin(internal); // best-effort; falls back to polling if it fails. Must
// happen before reinit_device() so REG_INT_CFG is written
// if IRQ setup succeeded.
}
// Best-effort: if the keyboard isn't attached yet (e.g. this device is constructed
// speculatively at boot so it can be hot-plug-detected later), these I2C writes fail
// silently and reinit_device() runs again once attach is detected.
reinit_device(device, internal);
internal->was_attached = is_attached_raw(device);
device_set_driver_data(device, internal);
return ERROR_NONE;
}
static error_t stop(Device* device) {
auto* internal = static_cast<Tab5KeyboardInternal*>(device_get_driver_data(device));
remove_irq_pin(internal);
write_reg(device, REG_INT_CFG, 0x00); // disable all interrupts
internal->sym_active = false;
internal->aa_sticky = false;
internal->aa_held = false;
update_leds(device, internal); // turn LEDs off
vQueueDelete(internal->queue);
free(internal);
device_set_driver_data(device, nullptr);
return ERROR_NONE;
}
// endregion
// region KeyboardApi
static error_t tab5_keyboard_read_key(Device* device, KeyboardKeyData* data) {
auto* internal = static_cast<Tab5KeyboardInternal*>(device_get_driver_data(device));
poll_if_due(device, internal);
uint32_t lv_key = 0;
if (xQueueReceive(internal->queue, &lv_key, 0) == pdTRUE) {
data->key = lv_key;
data->pressed = true;
data->continue_reading = uxQueueMessagesWaiting(internal->queue) > 0;
} else {
data->key = 0;
data->pressed = false;
data->continue_reading = false;
}
return ERROR_NONE;
}
// endregion
static const KeyboardApi tab5_keyboard_api = {
.read_key = tab5_keyboard_read_key,
};
// Defined in module.cpp - this driver is registered directly by m5stack-tab5's own module,
// not a separate Drivers/ module.
extern Module m5stack_tab5_module;
Driver tab5_keyboard_driver = {
.name = "tab5-keyboard",
.compatible = (const char*[]) { "m5stack,tab5-keyboard", nullptr },
.start_device = start,
.stop_device = stop,
.api = &tab5_keyboard_api,
.device_type = &KEYBOARD_TYPE,
.owner = &m5stack_tab5_module,
.internal = nullptr
};
// region Attach listener
void tab5_keyboard_add_attach_listener(Device* device, Tab5KeyboardAttachListener callback, void* context) {
auto* internal = static_cast<Tab5KeyboardInternal*>(device_get_driver_data(device));
if (internal->attach_listener != nullptr) {
LOG_W(TAG, "Replacing existing attach listener without it being removed first");
}
internal->attach_listener = callback;
internal->attach_listener_context = context;
}
void tab5_keyboard_remove_attach_listener(Device* device, Tab5KeyboardAttachListener callback) {
auto* internal = static_cast<Tab5KeyboardInternal*>(device_get_driver_data(device));
if (internal->attach_listener != callback) {
return;
}
internal->attach_listener = nullptr;
internal->attach_listener_context = nullptr;
}
// endregion
// region Dynamic construction
// Reacts to the Tab5 keyboard accessory's hot-plug attach state (see Tab5KeyboardAttachListener's
// doc comment above for the retry contract). This is UI-layer behavior the driver itself can't do
// (it has no LVGL dependency): switch to landscape while the keyboard is attached, restoring
// whatever rotation was active before once it's removed - but only if the user hasn't manually
// changed it since attaching, in which case their choice is respected. Ported as-is from the
// deprecated HAL's Tab5Keyboard::applyAutoRotation().
static bool on_keyboard_attach_changed(Device* /*device*/, bool attached, void* /*context*/) {
static lv_display_rotation_t saved_rotation = LV_DISPLAY_ROTATION_0;
static bool rotation_override_active = false;
auto* display = lv_display_get_default();
if (display == nullptr) {
return false; // LVGL not ready yet - retry on the next confirmed check
}
if (!lvgl_try_lock(pdMS_TO_TICKS(1000))) {
return false; // retry next check
}
if (attached) {
if (lv_display_get_rotation(display) != LV_DISPLAY_ROTATION_90) {
saved_rotation = lv_display_get_rotation(display);
rotation_override_active = true;
lv_display_set_rotation(display, LV_DISPLAY_ROTATION_90);
}
} else {
// Only restore if rotation is still what we set it to - if the user manually changed it
// since attaching, respect their choice instead.
if (rotation_override_active && lv_display_get_rotation(display) == LV_DISPLAY_ROTATION_90) {
lv_display_set_rotation(display, saved_rotation);
}
rotation_override_active = false;
}
lvgl_unlock();
return true;
}
static Tab5KeyboardConfig tab5_keyboard_config {};
static Device tab5_keyboard_device {};
// The keyboard accessory is a kernel driver device (m5stack,tab5-keyboard, defined directly in
// this project). Unlike the display/touch, it isn't gated on the display-variant detection at
// all (it lives on i2c2, a separate bus) - lvgl-module binds its indev unconditionally at boot
// regardless of physical attach state, and the driver's own read_key() polling handles hot-plug
// internally.
void tab5_create_keyboard(Device* i2c2) {
tab5_keyboard_device = Device {
.address = 0,
.name = "keyboard0",
.config = nullptr,
.parent = nullptr,
.internal = nullptr,
};
GpioPinSpec pin_interrupt = GPIO_PIN_SPEC_NONE;
Device* gpio0 = nullptr;
if (device_get_by_name("gpio0", &gpio0) == ERROR_NONE) {
pin_interrupt = GpioPinSpec { gpio0, 50, GPIO_FLAG_NONE };
device_put(gpio0);
} else {
LOG_W(TAG, "display_detect: gpio0 not found, keyboard will fall back to INT_STAT polling");
}
tab5_keyboard_config = Tab5KeyboardConfig {
.address = 0x6D,
.pin_interrupt = pin_interrupt,
};
tab5_keyboard_device.config = &tab5_keyboard_config;
// Parented to i2c2 itself (not root, unlike the display): the keyboard driver's start() uses
// device_get_parent() as its I2C bus controller.
if (construct_add_start(&tab5_keyboard_device, i2c2, "m5stack,tab5-keyboard")) {
tab5_keyboard_add_attach_listener(&tab5_keyboard_device, on_keyboard_attach_changed, nullptr);
}
}
// endregion
@@ -0,0 +1,46 @@
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
#include <stdbool.h>
#include <tactility/device.h>
#include <tactility/drivers/gpio.h>
struct Tab5KeyboardConfig {
// Fixed 0x6D on this keyboard accessory - kept as a field for parity/documentation with
// other dynamically-constructed devices in this project, not used to probe the bus.
uint8_t address;
// Native SoC GPIO (e.g. gpio0 pin 50) wired directly to the keyboard's INT line - not routed
// through an IO expander, since it needs a real hardware ISR for responsive key events.
// GPIO_PIN_SPEC_NONE falls back to polling REG_INT_STAT instead.
struct GpioPinSpec pin_interrupt;
};
// Called when the keyboard accessory's hot-plug attach state changes (confirmed over two
// consecutive ~1s checks - see the driver source). Orientation changes and other LVGL-aware
// reactions to attach state live outside the driver (it has no LVGL dependency) - module.cpp
// registers a listener for this instead.
// @return true once handled; false to be called again on the next confirmed check with the same
// `attached` value (e.g. if the LVGL lock couldn't be acquired) - mirrors this driver's own
// internal retry-until-handled pattern for reinitializing the device on reattach.
typedef bool (*Tab5KeyboardAttachListener)(struct Device* device, bool attached, void* context);
// Only one listener is supported (this board only ever has one caller - module.cpp). Registering
// a new one before removing the previous replaces it with a warning logged.
void tab5_keyboard_add_attach_listener(struct Device* device, Tab5KeyboardAttachListener callback, void* context);
void tab5_keyboard_remove_attach_listener(struct Device* device, Tab5KeyboardAttachListener callback);
extern struct Driver tab5_keyboard_driver;
// Constructs and starts the keyboard accessory device on i2c2, then registers this project's own
// hot-plug rotation handler as its attach listener. Called from display_detect.cpp's
// on_display_detect_event() once i2c2 is up.
void tab5_create_keyboard(struct Device* i2c2);
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,201 @@
// SPDX-License-Identifier: Apache-2.0
#include "tab5_power_control.h"
#include <tactility/device.h>
#include <tactility/driver.h>
#include <tactility/drivers/gpio_controller.h>
#include <tactility/drivers/power_supply.h>
#include <tactility/log.h>
#include <tactility/module.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <cstdlib>
#define TAG "Tab5PowerControl"
// GPIO expander 1 (0x44) pins - see the pinout table in Configuration.cpp. Only these three are
// this driver's concern; the rest (WLAN enable, USB-A 5V, nCHG_QC_EN's boot default, charge
// state LED) stay owned by Configuration.cpp's initExpander1().
static constexpr int GPIO_EXP1_PIN_DEVICE_POWER = 4; // PWROFF_PULSE
static constexpr int GPIO_EXP1_PIN_IP2326_NCHG_QC_EN = 5; // active-low: LOW = quick charge enabled
static constexpr int GPIO_EXP1_PIN_IP2326_CHG_EN = 7; // HIGH = charging enabled
struct Tab5PowerControlInternal {
bool charging_allowed;
bool quick_charge_enabled;
};
// region PowerSupplyApi
// No battery metrics here - see ina226-module's power-supply device for voltage/capacity.
static bool ps_supports_property(Device*, PowerSupplyProperty) {
return false;
}
static error_t ps_get_property(Device*, PowerSupplyProperty, PowerSupplyPropertyValue*) {
return ERROR_NOT_SUPPORTED;
}
static bool ps_supports_charge_control(Device*) {
return true;
}
static bool ps_is_allowed_to_charge(Device* device) {
auto* internal = static_cast<Tab5PowerControlInternal*>(device_get_driver_data(device));
return internal->charging_allowed;
}
static error_t ps_set_allowed_to_charge(Device* device, bool allowed) {
auto* internal = static_cast<Tab5PowerControlInternal*>(device_get_driver_data(device));
auto* io_expander1 = device_get_parent(device);
auto* pin = gpio_descriptor_acquire(io_expander1, GPIO_EXP1_PIN_IP2326_CHG_EN, GPIO_OWNER_GPIO);
if (pin == nullptr) {
LOG_W(TAG, "Failed to acquire CHG_EN pin");
return ERROR_RESOURCE;
}
error_t error = gpio_descriptor_set_flags(pin, GPIO_FLAG_DIRECTION_OUTPUT);
if (error == ERROR_NONE) {
error = gpio_descriptor_set_level(pin, allowed);
}
gpio_descriptor_release(pin);
if (error != ERROR_NONE) {
LOG_W(TAG, "Failed to set CHG_EN pin");
return error;
}
internal->charging_allowed = allowed;
return ERROR_NONE;
}
static bool ps_supports_quick_charge(Device*) {
return true;
}
static bool ps_is_quick_charge_enabled(Device* device) {
auto* internal = static_cast<Tab5PowerControlInternal*>(device_get_driver_data(device));
return internal->quick_charge_enabled;
}
static error_t ps_set_quick_charge_enabled(Device* device, bool enabled) {
auto* internal = static_cast<Tab5PowerControlInternal*>(device_get_driver_data(device));
auto* io_expander1 = device_get_parent(device);
auto* pin = gpio_descriptor_acquire(io_expander1, GPIO_EXP1_PIN_IP2326_NCHG_QC_EN, GPIO_OWNER_GPIO);
if (pin == nullptr) {
LOG_W(TAG, "Failed to acquire nCHG_QC_EN pin");
return ERROR_RESOURCE;
}
error_t error = gpio_descriptor_set_flags(pin, GPIO_FLAG_DIRECTION_OUTPUT);
if (error == ERROR_NONE) {
error = gpio_descriptor_set_level(pin, !enabled); // active-low
}
gpio_descriptor_release(pin);
if (error != ERROR_NONE) {
LOG_W(TAG, "Failed to set nCHG_QC_EN pin");
return error;
}
internal->quick_charge_enabled = enabled;
return ERROR_NONE;
}
static bool ps_supports_power_off(Device*) {
return true;
}
static error_t ps_power_off(Device* device) {
auto* io_expander1 = device_get_parent(device);
auto* pin = gpio_descriptor_acquire(io_expander1, GPIO_EXP1_PIN_DEVICE_POWER, GPIO_OWNER_GPIO);
if (pin == nullptr) {
LOG_E(TAG, "Failed to acquire DEVICE_POWER pin");
return ERROR_RESOURCE;
}
for (int i = 0; i < 3; i++) {
gpio_descriptor_set_level(pin, true);
vTaskDelay(pdMS_TO_TICKS(100));
gpio_descriptor_set_level(pin, false);
vTaskDelay(pdMS_TO_TICKS(100));
}
gpio_descriptor_release(pin);
return ERROR_NONE;
}
static constexpr PowerSupplyApi TAB5_POWER_CONTROL_API = {
.supports_property = ps_supports_property,
.get_property = ps_get_property,
.supports_charge_control = ps_supports_charge_control,
.is_allowed_to_charge = ps_is_allowed_to_charge,
.set_allowed_to_charge = ps_set_allowed_to_charge,
.supports_quick_charge = ps_supports_quick_charge,
.is_quick_charge_enabled = ps_is_quick_charge_enabled,
.set_quick_charge_enabled = ps_set_quick_charge_enabled,
.supports_power_off = ps_supports_power_off,
.power_off = ps_power_off,
};
// endregion
// region Driver lifecycle
static error_t start(Device* device) {
auto* internal = static_cast<Tab5PowerControlInternal*>(malloc(sizeof(Tab5PowerControlInternal)));
if (internal == nullptr) {
return ERROR_OUT_OF_MEMORY;
}
internal->charging_allowed = false;
internal->quick_charge_enabled = false;
device_set_driver_data(device, internal);
// DEVICE_POWER (PWROFF_PULSE) is only ever pulsed on demand (see ps_power_off) - configure it
// as an idle-low output up front so it's in a known state (previously done by Configuration.cpp's
// initExpander1(), now owned here - see that function's comment for why it no longer touches it).
auto* io_expander1 = device_get_parent(device);
auto* device_power_pin = gpio_descriptor_acquire(io_expander1, GPIO_EXP1_PIN_DEVICE_POWER, GPIO_OWNER_GPIO);
if (device_power_pin != nullptr) {
gpio_descriptor_set_flags(device_power_pin, GPIO_FLAG_DIRECTION_OUTPUT);
gpio_descriptor_set_level(device_power_pin, false);
gpio_descriptor_release(device_power_pin);
} else {
LOG_W(TAG, "Failed to acquire DEVICE_POWER pin at start");
}
// Quick charge off by default, matching the deprecated HAL's boot-time default (set via
// initExpander1(), now owned here for the same reason as above).
if (ps_set_quick_charge_enabled(device, false) != ERROR_NONE) {
LOG_W(TAG, "Failed to set default quick-charge state");
}
// Enable charging by default, matching the deprecated HAL's Tab5Power constructor.
if (ps_set_allowed_to_charge(device, true) != ERROR_NONE) {
LOG_W(TAG, "Failed to enable charging by default");
}
return ERROR_NONE;
}
static error_t stop(Device* device) {
auto* internal = static_cast<Tab5PowerControlInternal*>(device_get_driver_data(device));
free(internal);
device_set_driver_data(device, nullptr);
return ERROR_NONE;
}
// endregion
// Defined in module.cpp - this driver is registered directly by m5stack-tab5's own module,
// not a separate Drivers/ module (same pattern as Tab5KeyboardDriver).
extern Module m5stack_tab5_module;
Driver tab5_power_control_driver = {
.name = "tab5-power-control",
.compatible = (const char*[]) { "m5stack,tab5-power-control", nullptr },
.start_device = start,
.stop_device = stop,
.api = &TAB5_POWER_CONTROL_API,
.device_type = &POWER_SUPPLY_TYPE,
.owner = &m5stack_tab5_module,
.internal = nullptr
};
@@ -0,0 +1,19 @@
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#include <tactility/device.h>
// Board-specific charge-control/quick-charge/power-off glue for the IP2326 charger IC, driven
// through io_expander1 (PI4IOE5V6408 at 0x44) - separate from ina226's power-supply device
// (battery voltage/capacity, on the same board's INA226). Both are POWER_SUPPLY_TYPE kernel
// devices; consumers (Power.cpp, Statusbar.cpp, PowerOff.cpp) already enumerate every
// POWER_SUPPLY_TYPE device generically, so having two devices split by concern is the intended
// pattern rather than a gap - see module.cpp's create_tab5_power_control().
extern struct Driver tab5_power_control_driver;
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,30 @@
#include "tab_5_camera.h"
#include <tactility/log.h>
#include <esp_clock_output.h>
#define TAG "Tab5"
static esp_clock_output_mapping_handle_t camera_osc_handle = nullptr;
// 24 MHz clock on GPIO 36 for the SC2356 MIPI CSI sensor, required before esp_video_init. Uses
// SPLL (480 MHz) via esp_clock_output with divider 20 = 24 MHz exactly. This avoids any LEDC
// clock source conflict with the display backlight.
void tab5_camera_init() {
if (camera_osc_handle != nullptr) {
return;
}
if (esp_clock_output_start(CLKOUT_SIG_SPLL, GPIO_NUM_36, &camera_osc_handle) != ESP_OK) {
LOG_E(TAG, "Camera OSC clock output start failed");
return;
}
if (esp_clock_output_set_divider(camera_osc_handle, 20) != ESP_OK) {
LOG_E(TAG, "Camera OSC clock divider set failed");
esp_clock_output_stop(camera_osc_handle);
camera_osc_handle = nullptr;
return;
}
LOG_I(TAG, "Camera OSC 24MHz started on GPIO 36 (SPLL/20)");
}
@@ -0,0 +1,5 @@
#pragma once
// Starts the 24 MHz clock output on GPIO 36 the SC2356 MIPI CSI sensor needs before
// esp_video_init(). Called from module.cpp's start().
void tab5_camera_init();
+63 -141
View File
@@ -1,178 +1,100 @@
#include <tactility/module.h> #include <tactility/module.h>
#include <tactility/check.h>
#include <tactility/device.h> #include <tactility/device.h>
#include <tactility/device_listener.h>
#include <tactility/driver.h>
#include <tactility/drivers/gpio.h>
#include <tactility/drivers/gpio_controller.h> #include <tactility/drivers/gpio_controller.h>
#include <tactility/log.h> #include <tactility/log.h>
#include <Tactility/hal/keyboard/KeyboardDevice.h> #include <cstring>
#include "devices/Tab5Keyboard.h" #include "devices/detect.h"
#include "devices/tab5_headphone_detect.h"
#include <freertos/FreeRTOS.h> #include "devices/tab5_keyboard.h"
#include <freertos/timers.h> #include "devices/tab5_power_control.h"
#include "devices/tab_5_camera.h"
#include <atomic>
#define TAG "Tab5" #define TAG "Tab5"
// PI4IOE5V6408-0 (0x43) bit 1
constexpr auto GPIO_EXP0_PIN_SPEAKER_ENABLE = 1; constexpr auto GPIO_EXP0_PIN_SPEAKER_ENABLE = 1;
// PI4IOE5V6408-0 (0x43) bit 7
constexpr auto GPIO_EXP0_PIN_HEADPHONE_DETECT = 7; constexpr auto GPIO_EXP0_PIN_HEADPHONE_DETECT = 7;
constexpr auto HP_DETECT_POLL_MS = 1000;
constexpr auto KB_DETECT_POLL_MS = 1000;
// hp_detect_timer and kb_detect_timer are only touched from start()/stop(), which are called static void tab5_init_expander0(Device* io_expander0) {
// serially by the module manager — no atomic needed for the handles themselves. // Speaker and heapdhone pins are managed at runtime, so we can't have them as a hog in the dts file
static TimerHandle_t hp_detect_timer = nullptr; // We have to init them manually.
static TimerHandle_t kb_detect_timer = nullptr; auto* speaker_enable_pin = gpio_descriptor_acquire(io_expander0, GPIO_EXP0_PIN_SPEAKER_ENABLE, GPIO_OWNER_GPIO);
static std::atomic<Device*> io_expander0_cached { nullptr }; check(speaker_enable_pin);
// Flags are written by the timer daemon task and read by start()/stop() — use atomics. auto* headphone_detect_pin = gpio_descriptor_acquire(io_expander0, GPIO_EXP0_PIN_HEADPHONE_DETECT, GPIO_OWNER_GPIO);
static std::atomic<bool> hp_detect_last { false }; check(headphone_detect_pin);
static std::atomic<bool> hp_detect_initialized { false };
static std::atomic<bool> kb_late_started { false };
static void headphoneDetectCallback(TimerHandle_t /*timer*/) { gpio_descriptor_set_flags(speaker_enable_pin, GPIO_FLAG_DIRECTION_OUTPUT);
Device* cached = io_expander0_cached.load(std::memory_order_acquire); gpio_descriptor_set_flags(headphone_detect_pin, GPIO_FLAG_DIRECTION_INPUT);
if (!cached) {
cached = device_find_by_name("io_expander0"); gpio_descriptor_set_level(speaker_enable_pin, false);
io_expander0_cached.store(cached, std::memory_order_release);
} gpio_descriptor_release(speaker_enable_pin);
auto* io_expander0 = cached; gpio_descriptor_release(headphone_detect_pin);
if (!io_expander0) {
return; // Not ready yet, will retry on next tick
} }
auto* hp_pin = gpio_descriptor_acquire(io_expander0, GPIO_EXP0_PIN_HEADPHONE_DETECT, GPIO_OWNER_GPIO); static void tab5_enable_speaker_amp(Device* io_expander0) {
if (!hp_pin) { auto* speaker_enable_pin = gpio_descriptor_acquire(io_expander0, GPIO_EXP0_PIN_SPEAKER_ENABLE, GPIO_OWNER_GPIO);
LOG_W(TAG, "hp_detect: HP_DET pin busy"); check(speaker_enable_pin, "Failed to acquire speaker enable pin");
error_t error = gpio_descriptor_set_level(speaker_enable_pin, true);
gpio_descriptor_release(speaker_enable_pin);
if (error != ERROR_NONE) {
LOG_E(TAG, "Failed to enable amplifier: %s", error_to_string(error));
}
}
// Fires for every device's start/stop in the system; initializes io_expander0's pins and enables
// the speaker amplifier once io_expander0 itself has started.
static void on_io_expander0_started(Device* device, DeviceEvent event, void* context) {
(void)context;
static bool did_init = false;
if (did_init || event != DEVICE_EVENT_STARTED || strcmp(device->name, "io_expander0") != 0) {
return; return;
} }
did_init = true;
bool hp = false; tab5_init_expander0(device);
error_t err = gpio_descriptor_get_level(hp_pin, &hp); tab5_enable_speaker_amp(device);
gpio_descriptor_release(hp_pin);
if (err != ERROR_NONE) {
LOG_W(TAG, "hp_detect: HP_DET read error: %s", error_to_string(err));
return;
}
LOG_D(TAG, "hp_detect: HP_DET=%d", (int)hp);
if (!hp_detect_initialized || hp != hp_detect_last) {
auto* spk_pin = gpio_descriptor_acquire(io_expander0, GPIO_EXP0_PIN_SPEAKER_ENABLE, GPIO_OWNER_GPIO);
if (!spk_pin) {
LOG_W(TAG, "hp_detect: SPK_EN pin busy, will retry");
return;
}
error_t spk_err = gpio_descriptor_set_level(spk_pin, !hp);
gpio_descriptor_release(spk_pin);
if (spk_err != ERROR_NONE) {
LOG_W(TAG, "hp_detect: SPK_EN set error: %s, will retry", error_to_string(spk_err));
return;
}
hp_detect_last = hp;
hp_detect_initialized = true;
LOG_I(TAG, "Headphones %s, speaker %s", hp ? "detected" : "removed", hp ? "disabled" : "enabled");
}
}
// Detects a Tab5 Keyboard add-on that was plugged in after boot (so it wasn't started by
// Lvgl.cpp's attachDevices()). Once lateStart() succeeds, this stops polling for good — there's
// no support for re-detecting after the indev is torn down again.
static void keyboardDetectCallback(TimerHandle_t timer) {
if (kb_late_started.load(std::memory_order_acquire)) {
xTimerStop(timer, 0);
return;
}
using namespace tt::hal;
auto keyboard = findFirstDevice<keyboard::KeyboardDevice>(tt::hal::Device::Type::Keyboard);
if (!keyboard) {
return; // Not registered yet, will retry on next tick
}
if (keyboard->getLvglIndev() != nullptr) {
// Already started (boot-time attach) — nothing left to do.
kb_late_started.store(true, std::memory_order_release);
xTimerStop(timer, 0);
return;
}
if (!keyboard->isAttached()) {
return; // Not plugged in yet, will retry on next tick
}
auto tab5_keyboard = std::static_pointer_cast<Tab5Keyboard>(keyboard);
if (tab5_keyboard->lateStart()) {
LOG_I(TAG, "kb_detect: keyboard attached post-boot, LVGL input started");
kb_late_started.store(true, std::memory_order_release);
xTimerStop(timer, 0);
}
} }
extern "C" { extern "C" {
static error_t start() { static error_t start() {
if (hp_detect_timer != nullptr) { /* Crash when construct fails, because if a single driver fails to construct,
LOG_W(TAG, "hp_detect timer already running"); * there is no guarantee that the previously constructed drivers can be destroyed */
return ERROR_NONE; check(driver_construct_add(&tab5_keyboard_driver) == ERROR_NONE);
} check(driver_construct_add(&tab5_power_control_driver) == ERROR_NONE);
hp_detect_initialized = false; tab5_detect_start();
hp_detect_last = false;
hp_detect_timer = xTimerCreate("hp_detect", pdMS_TO_TICKS(HP_DETECT_POLL_MS), pdTRUE, nullptr, headphoneDetectCallback); tab5_camera_init();
if (!hp_detect_timer) {
LOG_E(TAG, "Failed to create hp_detect timer");
return ERROR_RESOURCE;
}
if (xTimerStart(hp_detect_timer, pdMS_TO_TICKS(100)) != pdPASS) {
LOG_E(TAG, "Failed to start hp_detect timer");
xTimerDelete(hp_detect_timer, pdMS_TO_TICKS(100));
hp_detect_timer = nullptr;
return ERROR_RESOURCE;
}
kb_late_started = false; device_listener_add(on_io_expander0_started, nullptr);
kb_detect_timer = xTimerCreate("kb_detect", pdMS_TO_TICKS(KB_DETECT_POLL_MS), pdTRUE, nullptr, keyboardDetectCallback); tab5_headphone_detect_start();
if (!kb_detect_timer) {
LOG_E(TAG, "Failed to create kb_detect timer");
return ERROR_RESOURCE;
}
if (xTimerStart(kb_detect_timer, pdMS_TO_TICKS(100)) != pdPASS) {
LOG_E(TAG, "Failed to start kb_detect timer");
xTimerDelete(kb_detect_timer, pdMS_TO_TICKS(100));
kb_detect_timer = nullptr;
return ERROR_RESOURCE;
}
return ERROR_NONE; return ERROR_NONE;
} }
static error_t stop() { static error_t stop() {
if (hp_detect_timer != nullptr) { tab5_headphone_detect_stop();
if (xTimerStop(hp_detect_timer, pdMS_TO_TICKS(100)) != pdPASS) {
LOG_W(TAG, "Failed to stop hp_detect timer");
}
if (xTimerDelete(hp_detect_timer, pdMS_TO_TICKS(100)) != pdPASS) {
LOG_E(TAG, "Failed to delete hp_detect timer");
}
// Always clear the handle — stale non-null handle is worse than a resource leak,
// as it would cause start() to silently skip re-creating the timer.
hp_detect_timer = nullptr;
io_expander0_cached.store(nullptr, std::memory_order_release);
}
if (kb_detect_timer != nullptr) { device_listener_remove(on_io_expander0_started);
if (xTimerStop(kb_detect_timer, pdMS_TO_TICKS(100)) != pdPASS) {
LOG_W(TAG, "Failed to stop kb_detect timer"); tab5_detect_stop();
}
if (xTimerDelete(kb_detect_timer, pdMS_TO_TICKS(100)) != pdPASS) { /* Crash when destruct fails, because if a single driver fails to destruct,
LOG_E(TAG, "Failed to delete kb_detect timer"); * there is no guarantee that the previously destroyed drivers can be recovered */
} check(driver_remove_destruct(&tab5_keyboard_driver) == ERROR_NONE);
kb_detect_timer = nullptr; check(driver_remove_destruct(&tab5_power_control_driver) == ERROR_NONE);
}
return ERROR_NONE; return ERROR_NONE;
} }
+5
View File
@@ -15,6 +15,8 @@ hardware.tinyUsb=true
storage.userDataLocation=SD storage.userDataLocation=SD
dependencies.useDeprecatedHal=false
display.size=5" display.size=5"
display.shape=rectangle display.shape=rectangle
display.dpi=294 display.dpi=294
@@ -39,6 +41,9 @@ sdkconfig.CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D3_4BIT_BUS_SLOT_1=8
sdkconfig.CONFIG_ESP_HOSTED_SDIO_GPIO_RESET_SLAVE=15 sdkconfig.CONFIG_ESP_HOSTED_SDIO_GPIO_RESET_SLAVE=15
# Fixes recent changes to esp_hosted # Fixes recent changes to esp_hosted
sdkconfig.CONFIG_ESP_HOSTED_USE_MEMPOOL=n sdkconfig.CONFIG_ESP_HOSTED_USE_MEMPOOL=n
# ESP-NOW bridge (EspNowBackendHosted.cpp) registers 6 custom-RPC handlers (4 resp + 2 event); default cap is 3
sdkconfig.CONFIG_ESP_HOSTED_ENABLE_PEER_DATA_TRANSFER=y
sdkconfig.CONFIG_ESP_HOSTED_MAX_CUSTOM_MSG_HANDLERS=8
# Performance: larger L2 cache reduces PSRAM stalls for draw/DPI buffers # Performance: larger L2 cache reduces PSRAM stalls for draw/DPI buffers
sdkconfig.CONFIG_CACHE_L2_CACHE_256KB=y sdkconfig.CONFIG_CACHE_L2_CACHE_256KB=y
# Performance: use P4's PPA (pixel processing accelerator for rotation) # Performance: use P4's PPA (pixel processing accelerator for rotation)
+3
View File
@@ -8,4 +8,7 @@ dependencies:
- Drivers/ina226-module - Drivers/ina226-module
- Drivers/rx8130ce-module - Drivers/rx8130ce-module
- Drivers/sc2356-module - Drivers/sc2356-module
- Drivers/ili9881c-module
- Drivers/st7123-module
- Drivers/gt911-module
dts: m5stack,tab5.dts dts: m5stack,tab5.dts
+58 -5
View File
@@ -6,10 +6,13 @@
#include <tactility/bindings/esp32_grove.h> #include <tactility/bindings/esp32_grove.h>
#include <tactility/bindings/esp32_i2c_master.h> #include <tactility/bindings/esp32_i2c_master.h>
#include <tactility/bindings/esp32_i2s.h> #include <tactility/bindings/esp32_i2s.h>
#include <tactility/bindings/esp32_pwm_ledc.h>
#include <tactility/bindings/esp32_sdmmc.h> #include <tactility/bindings/esp32_sdmmc.h>
#include <tactility/bindings/esp32_uart.h> #include <tactility/bindings/esp32_uart.h>
#include <tactility/bindings/esp32_usbhost.h> #include <tactility/bindings/esp32_usbhost.h>
#include <tactility/bindings/esp32_wifi.h> #include <tactility/bindings/esp32_wifi.h>
#include <tactility/bindings/gpio_hog.h>
#include <tactility/bindings/pwm_backlight.h>
#include <bindings/bmi270.h> #include <bindings/bmi270.h>
#include <bindings/es7210.h> #include <bindings/es7210.h>
#include <bindings/es8388.h> #include <bindings/es8388.h>
@@ -37,6 +40,19 @@
gpio-count = <57>; gpio-count = <57>;
}; };
display_backlight_pwm {
compatible = "espressif,esp32-pwm-ledc";
pin = <&gpio0 22 GPIO_FLAG_NONE>;
period-ns = <200000>; // 5000 Hz
ledc-timer = <1>;
ledc-channel = <0>;
};
display_backlight {
compatible = "pwm-backlight";
pwm = <&display_backlight_pwm>;
};
// Declared before i2c0 so it starts before the es8388/es7210 codecs that reference it // Declared before i2c0 so it starts before the es8388/es7210 codecs that reference it
i2s0 { i2s0 {
compatible = "espressif,esp32-i2s"; compatible = "espressif,esp32-i2s";
@@ -55,12 +71,12 @@
pin-sda = <&gpio0 31 GPIO_FLAG_PULL_UP>; pin-sda = <&gpio0 31 GPIO_FLAG_PULL_UP>;
pin-scl = <&gpio0 32 GPIO_FLAG_PULL_UP>; pin-scl = <&gpio0 32 GPIO_FLAG_PULL_UP>;
io_expander0 { io_expander0: io_expander0 {
compatible = "diodes,pi4ioe5v6408"; compatible = "diodes,pi4ioe5v6408";
reg = <0x43>; reg = <0x43>;
}; };
io_expander1 { io_expander1: io_expander1 {
compatible = "diodes,pi4ioe5v6408"; compatible = "diodes,pi4ioe5v6408";
reg = <0x44>; reg = <0x44>;
}; };
@@ -98,6 +114,10 @@
compatible = "ti,ina226"; compatible = "ti,ina226";
reg = <0x41>; reg = <0x41>;
shunt-milliohms = <5>; shunt-milliohms = <5>;
// 2S NP-F550 pack
power-supply;
power-supply-reference-voltage-mv = <8400>;
power-supply-min-voltage-mv = <6400>;
}; };
sc2356 { sc2356 {
@@ -106,6 +126,42 @@
}; };
}; };
exp0_rf_internal_external {
compatible = "gpio-hog";
pin = <&io_expander0 0 GPIO_FLAG_NONE>;
mode = <GPIO_HOG_MODE_OUTPUT_LOW>;
};
exp0_external_5v_bus_enable {
compatible = "gpio-hog";
pin = <&io_expander0 2 GPIO_FLAG_NONE>;
mode = <GPIO_HOG_MODE_OUTPUT_HIGH>;
};
exp0_camera_reset {
compatible = "gpio-hog";
pin = <&io_expander0 6 GPIO_FLAG_NONE>;
mode = <GPIO_HOG_MODE_OUTPUT_HIGH>;
};
exp1_c6_wlan_enable {
compatible = "gpio-hog";
pin = <&io_expander1 0 GPIO_FLAG_NONE>;
mode = <GPIO_HOG_MODE_OUTPUT_HIGH>;
};
exp1_usb_a_5v_enable {
compatible = "gpio-hog";
pin = <&io_expander1 3 GPIO_FLAG_NONE>;
mode = <GPIO_HOG_MODE_OUTPUT_HIGH>;
};
exp1_ip2326_chg_stat_led {
compatible = "gpio-hog";
pin = <&io_expander1 6 GPIO_FLAG_NONE>;
mode = <GPIO_HOG_MODE_OUTPUT_LOW>;
};
port_a: grove0 { port_a: grove0 {
compatible = "espressif,esp32-grove"; compatible = "espressif,esp32-grove";
defaultMode = <GROVE_MODE_I2C>; defaultMode = <GROVE_MODE_I2C>;
@@ -145,17 +201,14 @@
usbhosthid0 { usbhosthid0 {
compatible = "espressif,esp32-usbhost-hid"; compatible = "espressif,esp32-usbhost-hid";
//status = "disabled";
}; };
usbhostmidi0 { usbhostmidi0 {
compatible = "espressif,esp32-usbhost-midi"; compatible = "espressif,esp32-usbhost-midi";
//status = "disabled";
}; };
usbhostmsc0 { usbhostmsc0 {
compatible = "espressif,esp32-usbhost-msc"; compatible = "espressif,esp32-usbhost-msc";
//status = "disabled";
}; };
}; };
}; };
@@ -1,7 +1,7 @@
file(GLOB_RECURSE SOURCE_FILES Source/*.c*) file(GLOB_RECURSE SOURCE_FILES source/*.c*)
idf_component_register( idf_component_register(
SRCS ${SOURCE_FILES} SRCS ${SOURCE_FILES}
INCLUDE_DIRS "Source" INCLUDE_DIRS "source"
REQUIRES Tactility ButtonControl ST7789 PwmBacklight driver REQUIRES TactilityKernel driver
) )
@@ -1,23 +0,0 @@
#include "devices/Display.h"
#include <Tactility/hal/Configuration.h>
#include <PwmBacklight.h>
#include <ButtonControl.h>
using namespace tt::hal;
static DeviceVector createDevices() {
return {
createDisplay(),
ButtonControl::createOneButtonControl(0)
};
}
static bool initBoot() {
return driver::pwmbacklight::init(LCD_PIN_BACKLIGHT);
}
extern const Configuration hardwareConfiguration = {
.initBoot = initBoot,
.createDevices = createDevices
};
@@ -1,32 +0,0 @@
#include "Display.h"
#include <PwmBacklight.h>
#include <St7789Display.h>
std::shared_ptr<tt::hal::display::DisplayDevice> createDisplay() {
St7789Display::Configuration panel_configuration = {
.horizontalResolution = LCD_HORIZONTAL_RESOLUTION,
.verticalResolution = LCD_VERTICAL_RESOLUTION,
.gapX = 52,
.gapY = 40,
.swapXY = false,
.mirrorX = true,
.mirrorY = true,
.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>(St7789Display::SpiConfiguration {
.spiHostDevice = LCD_SPI_HOST,
.csPin = LCD_PIN_CS,
.dcPin = LCD_PIN_DC,
.pixelClockFrequency = 62'500'000,
.transactionQueueDepth = 10
});
return std::make_shared<St7789Display>(panel_configuration, spi_configuration);
}
@@ -1,18 +0,0 @@
#pragma once
#include <Tactility/hal/display/DisplayDevice.h>
#include <memory>
#include <driver/gpio.h>
#include <driver/spi_common.h>
constexpr auto LCD_SPI_HOST = SPI2_HOST;
constexpr auto LCD_PIN_CS = GPIO_NUM_10;
constexpr auto LCD_PIN_DC = GPIO_NUM_8;
constexpr auto LCD_PIN_RESET = GPIO_NUM_9;
constexpr auto LCD_PIN_BACKLIGHT = GPIO_NUM_7;
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<tt::hal::display::DisplayDevice> createDisplay();
@@ -14,6 +14,8 @@ hardware.tinyUsb=true
hardware.esptoolFlashFreq=120M hardware.esptoolFlashFreq=120M
hardware.bluetooth=true hardware.bluetooth=true
dependencies.useDeprecatedHal=false
storage.userDataLocation=SD storage.userDataLocation=SD
display.size=1.14" display.size=1.14"
@@ -1,3 +1,5 @@
dependencies: dependencies:
- Platforms/platform-esp32 - Platforms/platform-esp32
- Drivers/st7789-module
- Drivers/button-control-module
dts: waveshare,esp32-s3-geek.dts dts: waveshare,esp32-s3-geek.dts
@@ -8,7 +8,11 @@
#include <tactility/bindings/esp32_sdmmc.h> #include <tactility/bindings/esp32_sdmmc.h>
#include <tactility/bindings/esp32_spi.h> #include <tactility/bindings/esp32_spi.h>
#include <tactility/bindings/esp32_uart.h> #include <tactility/bindings/esp32_uart.h>
#include <tactility/bindings/display_placeholder.h> #include <tactility/bindings/esp32_pwm_ledc.h>
#include <tactility/bindings/pwm_backlight.h>
#include <bindings/st7789.h>
#include <bindings/button_control.h>
/ { / {
compatible = "root"; compatible = "root";
@@ -37,6 +41,22 @@
pin-scl = <&gpio0 17 GPIO_FLAG_NONE>; pin-scl = <&gpio0 17 GPIO_FLAG_NONE>;
}; };
display_backlight_pwm {
compatible = "espressif,esp32-pwm-ledc";
pin = <&gpio0 7 GPIO_FLAG_NONE>;
period-ns = <1953125>;
ledc-timer = <0>;
ledc-channel = <0>;
};
display_backlight {
compatible = "pwm-backlight";
// Off by default so display power-on won't show the screen from before the last power loss.
// The display backlight is turned on during the boot process.
status = "disabled";
pwm = <&display_backlight_pwm>;
};
spi0 { spi0 {
compatible = "espressif,esp32-spi"; compatible = "espressif,esp32-spi";
host = <SPI2_HOST>; host = <SPI2_HOST>;
@@ -44,11 +64,27 @@
pin-mosi = <&gpio0 11 GPIO_FLAG_NONE>; pin-mosi = <&gpio0 11 GPIO_FLAG_NONE>;
pin-sclk = <&gpio0 12 GPIO_FLAG_NONE>; pin-sclk = <&gpio0 12 GPIO_FLAG_NONE>;
display { display@0 {
compatible = "display-placeholder"; compatible = "sitronix,st7789";
horizontal-resolution = <135>;
vertical-resolution = <240>;
gap-x = <52>;
gap-y = <40>;
mirror-x;
mirror-y;
invert-color;
pixel-clock-hz = <62500000>;
pin-dc = <&gpio0 8 GPIO_FLAG_NONE>;
pin-reset = <&gpio0 9 GPIO_FLAG_NONE>;
backlight = <&display_backlight>;
}; };
}; };
buttons {
compatible = "tactility,button-control";
pin-primary = <&gpio0 0 GPIO_FLAG_NONE>;
};
sdmmc0 { sdmmc0 {
compatible = "espressif,esp32-sdmmc"; compatible = "espressif,esp32-sdmmc";
pin-clk = <&gpio0 36 GPIO_FLAG_NONE>; pin-clk = <&gpio0 36 GPIO_FLAG_NONE>;
@@ -0,0 +1,8 @@
# Force CMake reload to detect new files module.cpp and Configuration.cpp
file(GLOB_RECURSE SOURCE_FILES Source/*.c*)
idf_component_register(
SRCS ${SOURCE_FILES}
INCLUDE_DIRS "Source"
REQUIRES Tactility ButtonControl ST7305 PwmBacklight driver
)
@@ -0,0 +1,41 @@
#include "devices/Display.h"
#include <Tactility/hal/Configuration.h>
#include <ButtonControl.h>
#include <driver/gpio.h>
#include <tactility/log.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
static constexpr auto* TAG = "WaveshareRLCD";
using namespace tt::hal;
static DeviceVector createDevices() {
return {
createDisplay(),
ButtonControl::createTwoButtonControl(GPIO_NUM_18, GPIO_NUM_0) // KEY=18 cycles, BOOT=0 selects
};
}
static bool initBoot() {
// Amp enable GPIO46 active HIGH - keep LOW during boot to avoid pop,
// then HIGH after delay so speaker works for MCP / Mp3Player
gpio_config_t io_conf = {};
io_conf.intr_type = GPIO_INTR_DISABLE;
io_conf.mode = GPIO_MODE_OUTPUT;
io_conf.pin_bit_mask = (1ULL << GPIO_NUM_46);
io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE;
io_conf.pull_up_en = GPIO_PULLUP_DISABLE;
gpio_config(&io_conf);
gpio_set_level(GPIO_NUM_46, 0);
vTaskDelay(pdMS_TO_TICKS(100));
gpio_set_level(GPIO_NUM_46, 1);
LOG_I(TAG, "Speaker amp GPIO46 enabled");
return true;
}
extern const Configuration hardwareConfiguration = {
.initBoot = initBoot,
.createDevices = createDevices
};
@@ -0,0 +1,23 @@
#include "Display.h"
#include <St7305Display.h>
std::shared_ptr<tt::hal::display::DisplayDevice> createDisplay() {
auto configuration = std::make_unique<St7305Display::Configuration>(
SPI2_HOST,
GPIO_NUM_40, // CS
GPIO_NUM_5, // DC
400, // Width
300, // Height
nullptr, // Touch device
false, // swapXY
false, // mirrorX
false, // mirrorY
true, // invertColor - initial true to match existing inverted look, user can toggle via Settings->Display
0 // bufferSize (0 = default, which is full screen = 120,000 pixels)
);
configuration->resetPin = GPIO_NUM_41;
auto display = std::make_shared<St7305Display>(std::move(configuration));
return std::reinterpret_pointer_cast<tt::hal::display::DisplayDevice>(display);
}
@@ -3,17 +3,15 @@
extern "C" { extern "C" {
static error_t start() { static error_t start() {
// Empty for now
return ERROR_NONE; return ERROR_NONE;
} }
static error_t stop() { static error_t stop() {
// Empty for now
return ERROR_NONE; return ERROR_NONE;
} }
struct Module m5stack_sticks3_module = { struct Module waveshare_esp32_s3_rlcd_module = {
.name = "m5stack-sticks3", .name = "waveshare-esp32-s3-rlcd",
.start = start, .start = start,
.stop = stop, .stop = stop,
.symbols = nullptr, .symbols = nullptr,
@@ -0,0 +1,26 @@
general.vendor=WaveShare
general.name=ESP32-S3-RLCD-4.2
general.incubating=true
apps.launcherAppId=Launcher
# apps.autoStartAppId=ApWebServer
hardware.target=ESP32S3
hardware.flashSize=16MB
hardware.spiRam=true
hardware.spiRamMode=OCT
hardware.spiRamSpeed=120M
hardware.tinyUsb=true
hardware.esptoolFlashFreq=120M
hardware.bluetooth=true
storage.userDataLocation=SD
display.size=4.2"
display.shape=rectangle
display.dpi=120
lvgl.colorDepth=16
lvgl.uiDensity=compact
lvgl.fontSize=20
lvgl.theme=DefaultDark
@@ -0,0 +1,3 @@
dependencies:
- Platforms/platform-esp32
dts: waveshare,esp32-s3-rlcd.dts
@@ -0,0 +1,78 @@
/dts-v1/;
#include <tactility/bindings/root.h>
#include <tactility/bindings/esp32_ble.h>
#include <tactility/bindings/esp32_gpio.h>
#include <tactility/bindings/esp32_i2c_master.h>
#include <tactility/bindings/esp32_sdmmc.h>
#include <tactility/bindings/esp32_spi.h>
#include <tactility/bindings/esp32_i2s.h>
#include <tactility/bindings/esp32_uart.h>
#include <tactility/bindings/display_placeholder.h>
/ {
compatible = "root";
model = "Waveshare ESP32-S3-RLCD-4.2";
ble0 {
compatible = "espressif,esp32-ble";
status = "disabled";
};
gpio0 {
compatible = "espressif,esp32-gpio";
gpio-count = <49>;
};
// Audio: ES7210 mic + speaker via I2S0
// Pins verified from working MicroPython board_config.py for WAVESHARE_RLCD:
// mclk=16, bclk=9, ws=45, tx=8 (DAC out), rx=10 (ES7210 mic in)
// NOTE: Display DC is GPIO5, so I2S must NOT use GPIO5 to avoid bus corruption
i2s0 {
compatible = "espressif,esp32-i2s";
port = <I2S_NUM_0>;
pin-bclk = <&gpio0 9 GPIO_FLAG_NONE>;
pin-ws = <&gpio0 45 GPIO_FLAG_NONE>;
pin-data-out = <&gpio0 8 GPIO_FLAG_NONE>;
pin-data-in = <&gpio0 10 GPIO_FLAG_NONE>;
pin-mclk = <&gpio0 16 GPIO_FLAG_NONE>;
};
i2c0 {
compatible = "espressif,esp32-i2c-master";
port = <I2C_NUM_0>;
clock-frequency = <400000>;
pin-sda = <&gpio0 13 GPIO_FLAG_NONE>;
pin-scl = <&gpio0 14 GPIO_FLAG_NONE>;
};
spi0 {
compatible = "espressif,esp32-spi";
host = <SPI2_HOST>;
cs-gpios = <&gpio0 40 GPIO_FLAG_NONE>;
pin-mosi = <&gpio0 12 GPIO_FLAG_NONE>;
pin-sclk = <&gpio0 11 GPIO_FLAG_NONE>;
display {
compatible = "display-placeholder";
};
};
sdmmc0 {
compatible = "espressif,esp32-sdmmc";
pin-clk = <&gpio0 38 GPIO_FLAG_NONE>;
pin-cmd = <&gpio0 21 GPIO_FLAG_NONE>;
pin-d0 = <&gpio0 39 GPIO_FLAG_NONE>;
pin-d3 = <&gpio0 17 GPIO_FLAG_NONE>;
slot = <SDMMC_HOST_SLOT_1>;
bus-width = <1>;
pullups;
};
uart0 {
compatible = "espressif,esp32-uart";
port = <UART_NUM_0>;
pin-tx = <&gpio0 43 GPIO_FLAG_NONE>;
pin-rx = <&gpio0 44 GPIO_FLAG_NONE>;
};
};
+4 -11
View File
@@ -1,19 +1,12 @@
#include <tactility/error.h>
#include <tactility/module.h> #include <tactility/module.h>
extern "C" { extern "C" {
static error_t start() { Module waveshare_s3_lcd_13_module = {
return ERROR_NONE;
}
static error_t stop() {
return ERROR_NONE;
}
struct Module waveshare_s3_lcd_13_module = {
.name = "waveshare-s3-lcd-13", .name = "waveshare-s3-lcd-13",
.start = start, .start = [] -> error_t { return ERROR_NONE; },
.stop = stop, .stop = [] -> error_t { return ERROR_NONE; },
.symbols = nullptr, .symbols = nullptr,
.internal = nullptr .internal = nullptr
}; };
@@ -1,7 +1,7 @@
file(GLOB_RECURSE SOURCE_FILES Source/*.c*) file(GLOB_RECURSE SOURCE_FILES source/*.c*)
idf_component_register( idf_component_register(
SRCS ${SOURCE_FILES} SRCS ${SOURCE_FILES}
INCLUDE_DIRS "Source" INCLUDE_DIRS "source"
REQUIRES Tactility esp_lvgl_port GC9A01 CST816S PwmBacklight driver REQUIRES TactilityKernel
) )
@@ -1,21 +0,0 @@
#include "devices/Display.h"
#include <Tactility/hal/Configuration.h>
#include <PwmBacklight.h>
using namespace tt::hal;
static DeviceVector createDevices() {
return {
createDisplay(),
};
}
static bool initBoot() {
return driver::pwmbacklight::init(GPIO_NUM_2, 256);
}
extern const Configuration hardwareConfiguration = {
.initBoot = initBoot,
.createDevices = createDevices
};
@@ -1,45 +0,0 @@
#include "Display.h"
#include <Cst816Touch.h>
#include <PwmBacklight.h>
#include <Gc9a01Display.h>
std::shared_ptr<tt::hal::touch::TouchDevice> createTouch() {
auto configuration = std::make_unique<Cst816sTouch::Configuration>(
I2C_NUM_0,
240,
240,
false,
true,
true,
GPIO_NUM_13,
GPIO_NUM_5
);
return std::make_shared<Cst816sTouch>(std::move(configuration));
}
std::shared_ptr<tt::hal::display::DisplayDevice> createDisplay() {
auto touch = createTouch();
auto configuration = std::make_unique<Gc9a01Display::Configuration>(
SPI2_HOST,
GPIO_NUM_9,
GPIO_NUM_8,
240,
240,
touch,
false,
false,
true,
true,
0,
LCD_RGB_ELEMENT_ORDER_BGR
);
configuration->resetPin = GPIO_NUM_14;
configuration->backlightDutyFunction = driver::pwmbacklight::setBacklightDuty;
auto display = std::make_shared<Gc9a01Display>(std::move(configuration));
return std::reinterpret_pointer_cast<tt::hal::display::DisplayDevice>(display);
}
@@ -1,5 +0,0 @@
#pragma once
#include "Tactility/hal/display/DisplayDevice.h"
std::shared_ptr<tt::hal::display::DisplayDevice> createDisplay();
@@ -1,23 +0,0 @@
#include <tactility/module.h>
extern "C" {
static error_t start() {
// Empty for now
return ERROR_NONE;
}
static error_t stop() {
// Empty for now
return ERROR_NONE;
}
struct Module waveshare_s3_touch_lcd_128_module = {
.name = "waveshare-s3-touch-lcd-128",
.start = start,
.stop = stop,
.symbols = nullptr,
.internal = nullptr
};
}
@@ -14,7 +14,9 @@ hardware.tinyUsb=true
hardware.esptoolFlashFreq=120M hardware.esptoolFlashFreq=120M
hardware.bluetooth=true hardware.bluetooth=true
storage.userDataLocation=SD dependencies.useDeprecatedHal=false
storage.userDataLocation=Internal
display.size=1.28" display.size=1.28"
display.shape=circle display.shape=circle

Some files were not shown because too many files have changed in this diff Show More