From 6d84e6eb328e4a168be3763e3bbad28efea867e7 Mon Sep 17 00:00:00 2001 From: Ken Van Hoeylandt Date: Mon, 20 Jul 2026 22:48:40 +0200 Subject: [PATCH] Device migrations, driver migrations and more --- Devices/btt-panda-touch/source/module.cpp | 13 +- Devices/cyd-2432s024c/source/module.cpp | 13 +- Devices/cyd-2432s024r/source/module.cpp | 13 +- Devices/cyd-2432s028r/source/module.cpp | 13 +- Devices/cyd-2432s028rv3/source/module.cpp | 13 +- Devices/cyd-2432s032c/source/module.cpp | 15 +- Devices/cyd-3248s035c/source/module.cpp | 15 +- Devices/cyd-4848s040c/source/module.cpp | 15 +- Devices/cyd-8048s043c/source/module.cpp | 15 +- Devices/cyd-e32r28t/source/module.cpp | 15 +- Devices/cyd-e32r32p/source/module.cpp | 15 +- .../source/module.cpp | 15 +- .../source/module.cpp | 13 +- .../source/module.cpp | 15 +- .../source/module.cpp | 15 +- .../source/module.cpp | 13 +- .../source/module.cpp | 15 +- Devices/es3c28p/source/module.cpp | 15 +- Devices/generic-esp32/source/module.cpp | 15 +- Devices/generic-esp32c6/source/module.cpp | 15 +- Devices/generic-esp32p4/source/module.cpp | 15 +- Devices/generic-esp32s3/source/module.cpp | 15 +- .../guition-jc1060p470ciwy/source/module.cpp | 13 +- Devices/guition-jc2432w328c/source/module.cpp | 15 +- Devices/guition-jc8048w550c/source/module.cpp | 15 +- .../heltec-wifi-lora-32-v3/source/module.cpp | 15 +- Devices/m5stack-stackchan/Source/module.cpp | 15 +- Devices/m5stack-sticks3/CMakeLists.txt | 6 +- .../m5stack-sticks3/Source/Configuration.cpp | 48 -- .../Source/devices/Display.cpp | 32 - .../m5stack-sticks3/Source/devices/Display.h | 17 - .../m5stack-sticks3/Source/devices/Power.cpp | 94 --- .../m5stack-sticks3/Source/devices/Power.h | 6 - Devices/m5stack-sticks3/Source/module.cpp | 23 - Devices/m5stack-sticks3/device.properties | 2 + Devices/m5stack-sticks3/devicetree.yaml | 2 + Devices/m5stack-sticks3/m5stack,sticks3.dts | 45 +- Devices/m5stack-sticks3/source/module.cpp | 14 + Devices/m5stack-tab5/CMakeLists.txt | 2 +- Devices/m5stack-tab5/Source/Configuration.cpp | 205 ------ .../m5stack-tab5/Source/devices/Detect.cpp | 45 -- Devices/m5stack-tab5/Source/devices/Detect.h | 8 - .../m5stack-tab5/Source/devices/Display.cpp | 108 --- Devices/m5stack-tab5/Source/devices/Display.h | 6 - .../Source/devices/Ili9881cDisplay.cpp | 154 ---- .../Source/devices/Ili9881cDisplay.h | 38 - Devices/m5stack-tab5/Source/devices/Power.cpp | 192 ----- Devices/m5stack-tab5/Source/devices/Power.h | 6 - .../Source/devices/St7123Display.cpp | 148 ---- .../Source/devices/St7123Display.h | 38 - .../Source/devices/St7123Touch.cpp | 41 -- .../m5stack-tab5/Source/devices/St7123Touch.h | 59 -- .../Source/devices/Tab5Keyboard.cpp | 561 -------------- .../Source/devices/Tab5Keyboard.h | 85 --- .../m5stack-tab5/Source/devices/detect.cpp | 48 ++ Devices/m5stack-tab5/Source/devices/detect.h | 37 + .../Source/devices/devices_common.cpp | 45 ++ .../Source/devices/devices_common.h | 29 + .../Source/devices/devices_v1.cpp | 145 ++++ .../m5stack-tab5/Source/devices/devices_v1.h | 5 + .../Source/devices/devices_v2.cpp | 124 ++++ .../m5stack-tab5/Source/devices/devices_v2.h | 5 + .../Source/devices/display_detect.cpp | 138 ++++ .../Source/devices/tab5_headphone_detect.cpp | 109 +++ .../Source/devices/tab5_headphone_detect.h | 6 + .../Source/devices/tab5_keyboard.cpp | 695 ++++++++++++++++++ .../Source/devices/tab5_keyboard.h | 46 ++ .../Source/devices/tab5_power_control.cpp | 201 +++++ .../Source/devices/tab5_power_control.h | 19 + .../Source/devices/tab_5_camera.cpp | 30 + .../Source/devices/tab_5_camera.h | 5 + Devices/m5stack-tab5/Source/module.cpp | 194 ++--- Devices/m5stack-tab5/device.properties | 2 + Devices/m5stack-tab5/devicetree.yaml | 3 + Devices/m5stack-tab5/m5stack,tab5.dts | 63 +- .../waveshare-esp32-s3-geek/CMakeLists.txt | 14 +- .../Source/Configuration.cpp | 23 - .../Source/devices/Display.cpp | 32 - .../Source/devices/Display.h | 18 - .../waveshare-esp32-s3-geek/device.properties | 2 + .../waveshare-esp32-s3-geek/devicetree.yaml | 2 + .../{Source => source}/module.cpp | 0 .../waveshare,esp32-s3-geek.dts | 44 +- Devices/waveshare-s3-lcd-13/source/module.cpp | 15 +- .../waveshare-s3-touch-lcd-128/CMakeLists.txt | 6 +- .../Source/Configuration.cpp | 21 - .../Source/devices/Display.cpp | 45 -- .../Source/devices/Display.h | 5 - .../Source/module.cpp | 23 - .../device.properties | 4 +- .../devicetree.yaml | 2 + .../source/module.cpp | 14 + .../waveshare,s3-touch-lcd-128.dts | 58 +- .../waveshare-s3-touch-lcd-147/CMakeLists.txt | 6 +- .../Source/Configuration.cpp | 18 - .../Source/Init.cpp | 45 -- .../Source/devices/Axs5106Touch.cpp | 35 - .../Source/devices/Axs5106Touch.h | 70 -- .../Source/devices/Display.cpp | 50 -- .../Source/devices/Display.h | 5 - .../Source/devices/Jd9853Display.cpp | 161 ---- .../Source/devices/Jd9853Display.h | 106 --- .../Source/module.cpp | 23 - .../device.properties | 5 +- .../devicetree.yaml | 3 + .../source/module.cpp | 14 + .../waveshare,s3-touch-lcd-147.dts | 50 +- .../waveshare-s3-touch-lcd-43/CMakeLists.txt | 6 +- .../Source/Configuration.cpp | 30 - .../Source/devices/Display.cpp | 106 --- .../Source/devices/Display.h | 5 - .../device.properties | 2 + .../waveshare-s3-touch-lcd-43/devicetree.yaml | 3 + .../{Source => source}/module.cpp | 15 +- .../waveshare,s3-touch-lcd-43.dts | 71 ++ .../CMakeLists.txt | 14 +- .../Source/Configuration.cpp | 21 - .../Source/devices/Display.cpp | 44 -- .../Source/devices/Display.h | 5 - .../Source/module.cpp | 23 - .../device.properties | 2 + .../devicetree.yaml | 2 + .../source/module.cpp | 14 + .../wireless-tag,wt32-sc01-plus.dts | 59 +- Drivers/axs5106-module/CMakeLists.txt | 11 + Drivers/axs5106-module/LICENSE-Apache-2.0.md | 195 +++++ Drivers/axs5106-module/README.md | 12 + .../axs5106-module/bindings/axs,axs5106.yaml | 53 ++ Drivers/axs5106-module/devicetree.yaml | 3 + .../axs5106-module/include/axs5106_module.h | 14 + .../axs5106-module/include/bindings/axs5106.h | 7 + .../axs5106-module/include/drivers/axs5106.h | 34 + Drivers/axs5106-module/source/axs5106.cpp | 245 ++++++ Drivers/axs5106-module/source/module.cpp | 32 + Drivers/ch422g-module/CMakeLists.txt | 11 + Drivers/ch422g-module/LICENSE-Apache-2.0.md | 195 +++++ Drivers/ch422g-module/README.md | 10 + .../ch422g-module/bindings/wch,ch422g.yaml | 8 + Drivers/ch422g-module/devicetree.yaml | 3 + .../ch422g-module/include/bindings/ch422g.h | 15 + Drivers/ch422g-module/include/ch422g_module.h | 14 + .../ch422g-module/include/drivers/ch422g.h | 18 + Drivers/ch422g-module/source/ch422g.cpp | 163 ++++ Drivers/ch422g-module/source/module.cpp | 32 + Drivers/cst816t-module/CMakeLists.txt | 11 + Drivers/cst816t-module/LICENSE-Apache-2.0.md | 195 +++++ Drivers/cst816t-module/README.md | 16 + .../bindings/hynitron,cst816t.yaml | 45 ++ Drivers/cst816t-module/devicetree.yaml | 3 + .../cst816t-module/include/bindings/cst816t.h | 7 + .../cst816t-module/include/cst816t_module.h | 14 + .../cst816t-module/include/drivers/cst816t.h | 28 + Drivers/cst816t-module/source/cst816t.cpp | 254 +++++++ Drivers/cst816t-module/source/module.cpp | 26 + Drivers/gc9a01-module/CMakeLists.txt | 11 + Drivers/gc9a01-module/LICENSE-Apache-2.0.md | 195 +++++ Drivers/gc9a01-module/README.md | 5 + .../bindings/galaxycore,gc9a01.yaml | 67 ++ Drivers/gc9a01-module/devicetree.yaml | 3 + .../gc9a01-module/include/bindings/gc9a01.h | 7 + .../gc9a01-module/include/drivers/gc9a01.h | 35 + Drivers/gc9a01-module/include/gc9a01_module.h | 14 + Drivers/gc9a01-module/source/gc9a01.cpp | 322 ++++++++ Drivers/gc9a01-module/source/module.cpp | 32 + Drivers/ili9881c-module/CMakeLists.txt | 11 + Drivers/ili9881c-module/LICENSE-Apache-2.0.md | 195 +++++ .../bindings/ilitek,ili9881c.yaml | 128 ++++ Drivers/ili9881c-module/devicetree.yaml | 3 + .../include/bindings/ili9881c.h | 10 + .../include/drivers/ili9881c.h | 71 ++ .../ili9881c-module/include/ili9881c_module.h | 14 + Drivers/ili9881c-module/source/ili9881c.cpp | 526 +++++++++++++ Drivers/ili9881c-module/source/module.cpp | 32 + Drivers/ina226-module/bindings/ti,ina226.yaml | 13 + .../ina226-module/include/drivers/ina226.h | 7 + Drivers/ina226-module/source/ina226.cpp | 151 +++- Drivers/ina226-module/source/module.cpp | 3 + Drivers/jd9853-module/CMakeLists.txt | 11 + Drivers/jd9853-module/LICENSE-Apache-2.0.md | 195 +++++ Drivers/jd9853-module/README.md | 5 + .../jd9853-module/bindings/jadard,jd9853.yaml | 73 ++ Drivers/jd9853-module/devicetree.yaml | 3 + .../jd9853-module/include/bindings/jd9853.h | 7 + .../jd9853-module/include/drivers/jd9853.h | 37 + Drivers/jd9853-module/include/jd9853_module.h | 14 + Drivers/jd9853-module/source/jd9853.cpp | 335 +++++++++ Drivers/jd9853-module/source/module.cpp | 32 + .../m5pm1-module/bindings/m5stack,m5pm1.yaml | 13 + Drivers/m5pm1-module/include/drivers/m5pm1.h | 4 + Drivers/m5pm1-module/source/m5pm1.cpp | 157 +++- Drivers/m5pm1-module/source/module.cpp | 3 + Drivers/st7123-module/CMakeLists.txt | 11 + Drivers/st7123-module/LICENSE-Apache-2.0.md | 195 +++++ Drivers/st7123-module/README.md | 18 + .../bindings/sitronix,st7123-touch.yaml | 47 ++ .../bindings/sitronix,st7123.yaml | 127 ++++ Drivers/st7123-module/devicetree.yaml | 3 + .../st7123-module/include/bindings/st7123.h | 10 + .../include/bindings/st7123_touch.h | 11 + .../st7123-module/include/drivers/st7123.h | 69 ++ .../include/drivers/st7123_touch.h | 34 + Drivers/st7123-module/include/st7123_module.h | 14 + Drivers/st7123-module/source/module.cpp | 35 + Drivers/st7123-module/source/st7123.cpp | 525 +++++++++++++ Drivers/st7123-module/source/st7123_touch.cpp | 210 ++++++ Drivers/st7796-i8080-module/CMakeLists.txt | 11 + .../st7796-i8080-module/LICENSE-Apache-2.0.md | 195 +++++ Drivers/st7796-i8080-module/README.md | 6 + .../bindings/sitronix,st7796-i8080.yaml | 69 ++ Drivers/st7796-i8080-module/devicetree.yaml | 3 + .../include/bindings/st7796_i8080.h | 7 + .../include/drivers/st7796_i8080.h | 36 + .../include/st7796_i8080_module.h | 14 + Drivers/st7796-i8080-module/source/module.cpp | 32 + .../source/st7796_i8080.cpp | 338 +++++++++ Modules/crypt-module/source/module.cpp | 13 +- Modules/lvgl-module/source/lvgl_devices.c | 9 + Modules/lvgl-module/source/lvgl_display.c | 31 +- Tactility/Source/app/poweroff/PowerOff.cpp | 4 +- .../Source/app/systeminfo/SystemInfo.cpp | 302 +------- Tactility/Source/lvgl/Keyboard.cpp | 13 +- 221 files changed, 8823 insertions(+), 3651 deletions(-) delete mode 100644 Devices/m5stack-sticks3/Source/Configuration.cpp delete mode 100644 Devices/m5stack-sticks3/Source/devices/Display.cpp delete mode 100644 Devices/m5stack-sticks3/Source/devices/Display.h delete mode 100644 Devices/m5stack-sticks3/Source/devices/Power.cpp delete mode 100644 Devices/m5stack-sticks3/Source/devices/Power.h delete mode 100644 Devices/m5stack-sticks3/Source/module.cpp create mode 100644 Devices/m5stack-sticks3/source/module.cpp delete mode 100644 Devices/m5stack-tab5/Source/Configuration.cpp delete mode 100644 Devices/m5stack-tab5/Source/devices/Detect.cpp delete mode 100644 Devices/m5stack-tab5/Source/devices/Detect.h delete mode 100644 Devices/m5stack-tab5/Source/devices/Display.cpp delete mode 100644 Devices/m5stack-tab5/Source/devices/Display.h delete mode 100644 Devices/m5stack-tab5/Source/devices/Ili9881cDisplay.cpp delete mode 100644 Devices/m5stack-tab5/Source/devices/Ili9881cDisplay.h delete mode 100644 Devices/m5stack-tab5/Source/devices/Power.cpp delete mode 100644 Devices/m5stack-tab5/Source/devices/Power.h delete mode 100644 Devices/m5stack-tab5/Source/devices/St7123Display.cpp delete mode 100644 Devices/m5stack-tab5/Source/devices/St7123Display.h delete mode 100644 Devices/m5stack-tab5/Source/devices/St7123Touch.cpp delete mode 100644 Devices/m5stack-tab5/Source/devices/St7123Touch.h delete mode 100644 Devices/m5stack-tab5/Source/devices/Tab5Keyboard.cpp delete mode 100644 Devices/m5stack-tab5/Source/devices/Tab5Keyboard.h create mode 100644 Devices/m5stack-tab5/Source/devices/detect.cpp create mode 100644 Devices/m5stack-tab5/Source/devices/detect.h create mode 100644 Devices/m5stack-tab5/Source/devices/devices_common.cpp create mode 100644 Devices/m5stack-tab5/Source/devices/devices_common.h create mode 100644 Devices/m5stack-tab5/Source/devices/devices_v1.cpp create mode 100644 Devices/m5stack-tab5/Source/devices/devices_v1.h create mode 100644 Devices/m5stack-tab5/Source/devices/devices_v2.cpp create mode 100644 Devices/m5stack-tab5/Source/devices/devices_v2.h create mode 100644 Devices/m5stack-tab5/Source/devices/display_detect.cpp create mode 100644 Devices/m5stack-tab5/Source/devices/tab5_headphone_detect.cpp create mode 100644 Devices/m5stack-tab5/Source/devices/tab5_headphone_detect.h create mode 100644 Devices/m5stack-tab5/Source/devices/tab5_keyboard.cpp create mode 100644 Devices/m5stack-tab5/Source/devices/tab5_keyboard.h create mode 100644 Devices/m5stack-tab5/Source/devices/tab5_power_control.cpp create mode 100644 Devices/m5stack-tab5/Source/devices/tab5_power_control.h create mode 100644 Devices/m5stack-tab5/Source/devices/tab_5_camera.cpp create mode 100644 Devices/m5stack-tab5/Source/devices/tab_5_camera.h delete mode 100644 Devices/waveshare-esp32-s3-geek/Source/Configuration.cpp delete mode 100644 Devices/waveshare-esp32-s3-geek/Source/devices/Display.cpp delete mode 100644 Devices/waveshare-esp32-s3-geek/Source/devices/Display.h rename Devices/waveshare-esp32-s3-geek/{Source => source}/module.cpp (100%) delete mode 100644 Devices/waveshare-s3-touch-lcd-128/Source/Configuration.cpp delete mode 100644 Devices/waveshare-s3-touch-lcd-128/Source/devices/Display.cpp delete mode 100644 Devices/waveshare-s3-touch-lcd-128/Source/devices/Display.h delete mode 100644 Devices/waveshare-s3-touch-lcd-128/Source/module.cpp create mode 100644 Devices/waveshare-s3-touch-lcd-128/source/module.cpp delete mode 100644 Devices/waveshare-s3-touch-lcd-147/Source/Configuration.cpp delete mode 100644 Devices/waveshare-s3-touch-lcd-147/Source/Init.cpp delete mode 100644 Devices/waveshare-s3-touch-lcd-147/Source/devices/Axs5106Touch.cpp delete mode 100644 Devices/waveshare-s3-touch-lcd-147/Source/devices/Axs5106Touch.h delete mode 100644 Devices/waveshare-s3-touch-lcd-147/Source/devices/Display.cpp delete mode 100644 Devices/waveshare-s3-touch-lcd-147/Source/devices/Display.h delete mode 100644 Devices/waveshare-s3-touch-lcd-147/Source/devices/Jd9853Display.cpp delete mode 100644 Devices/waveshare-s3-touch-lcd-147/Source/devices/Jd9853Display.h delete mode 100644 Devices/waveshare-s3-touch-lcd-147/Source/module.cpp create mode 100644 Devices/waveshare-s3-touch-lcd-147/source/module.cpp delete mode 100644 Devices/waveshare-s3-touch-lcd-43/Source/Configuration.cpp delete mode 100644 Devices/waveshare-s3-touch-lcd-43/Source/devices/Display.cpp delete mode 100644 Devices/waveshare-s3-touch-lcd-43/Source/devices/Display.h rename Devices/waveshare-s3-touch-lcd-43/{Source => source}/module.cpp (50%) delete mode 100644 Devices/wireless-tag-wt32-sc01-plus/Source/Configuration.cpp delete mode 100644 Devices/wireless-tag-wt32-sc01-plus/Source/devices/Display.cpp delete mode 100644 Devices/wireless-tag-wt32-sc01-plus/Source/devices/Display.h delete mode 100644 Devices/wireless-tag-wt32-sc01-plus/Source/module.cpp create mode 100644 Devices/wireless-tag-wt32-sc01-plus/source/module.cpp create mode 100644 Drivers/axs5106-module/CMakeLists.txt create mode 100644 Drivers/axs5106-module/LICENSE-Apache-2.0.md create mode 100644 Drivers/axs5106-module/README.md create mode 100644 Drivers/axs5106-module/bindings/axs,axs5106.yaml create mode 100644 Drivers/axs5106-module/devicetree.yaml create mode 100644 Drivers/axs5106-module/include/axs5106_module.h create mode 100644 Drivers/axs5106-module/include/bindings/axs5106.h create mode 100644 Drivers/axs5106-module/include/drivers/axs5106.h create mode 100644 Drivers/axs5106-module/source/axs5106.cpp create mode 100644 Drivers/axs5106-module/source/module.cpp create mode 100644 Drivers/ch422g-module/CMakeLists.txt create mode 100644 Drivers/ch422g-module/LICENSE-Apache-2.0.md create mode 100644 Drivers/ch422g-module/README.md create mode 100644 Drivers/ch422g-module/bindings/wch,ch422g.yaml create mode 100644 Drivers/ch422g-module/devicetree.yaml create mode 100644 Drivers/ch422g-module/include/bindings/ch422g.h create mode 100644 Drivers/ch422g-module/include/ch422g_module.h create mode 100644 Drivers/ch422g-module/include/drivers/ch422g.h create mode 100644 Drivers/ch422g-module/source/ch422g.cpp create mode 100644 Drivers/ch422g-module/source/module.cpp create mode 100644 Drivers/cst816t-module/CMakeLists.txt create mode 100644 Drivers/cst816t-module/LICENSE-Apache-2.0.md create mode 100644 Drivers/cst816t-module/README.md create mode 100644 Drivers/cst816t-module/bindings/hynitron,cst816t.yaml create mode 100644 Drivers/cst816t-module/devicetree.yaml create mode 100644 Drivers/cst816t-module/include/bindings/cst816t.h create mode 100644 Drivers/cst816t-module/include/cst816t_module.h create mode 100644 Drivers/cst816t-module/include/drivers/cst816t.h create mode 100644 Drivers/cst816t-module/source/cst816t.cpp create mode 100644 Drivers/cst816t-module/source/module.cpp create mode 100644 Drivers/gc9a01-module/CMakeLists.txt create mode 100644 Drivers/gc9a01-module/LICENSE-Apache-2.0.md create mode 100644 Drivers/gc9a01-module/README.md create mode 100644 Drivers/gc9a01-module/bindings/galaxycore,gc9a01.yaml create mode 100644 Drivers/gc9a01-module/devicetree.yaml create mode 100644 Drivers/gc9a01-module/include/bindings/gc9a01.h create mode 100644 Drivers/gc9a01-module/include/drivers/gc9a01.h create mode 100644 Drivers/gc9a01-module/include/gc9a01_module.h create mode 100644 Drivers/gc9a01-module/source/gc9a01.cpp create mode 100644 Drivers/gc9a01-module/source/module.cpp create mode 100644 Drivers/ili9881c-module/CMakeLists.txt create mode 100644 Drivers/ili9881c-module/LICENSE-Apache-2.0.md create mode 100644 Drivers/ili9881c-module/bindings/ilitek,ili9881c.yaml create mode 100644 Drivers/ili9881c-module/devicetree.yaml create mode 100644 Drivers/ili9881c-module/include/bindings/ili9881c.h create mode 100644 Drivers/ili9881c-module/include/drivers/ili9881c.h create mode 100644 Drivers/ili9881c-module/include/ili9881c_module.h create mode 100644 Drivers/ili9881c-module/source/ili9881c.cpp create mode 100644 Drivers/ili9881c-module/source/module.cpp create mode 100644 Drivers/jd9853-module/CMakeLists.txt create mode 100644 Drivers/jd9853-module/LICENSE-Apache-2.0.md create mode 100644 Drivers/jd9853-module/README.md create mode 100644 Drivers/jd9853-module/bindings/jadard,jd9853.yaml create mode 100644 Drivers/jd9853-module/devicetree.yaml create mode 100644 Drivers/jd9853-module/include/bindings/jd9853.h create mode 100644 Drivers/jd9853-module/include/drivers/jd9853.h create mode 100644 Drivers/jd9853-module/include/jd9853_module.h create mode 100644 Drivers/jd9853-module/source/jd9853.cpp create mode 100644 Drivers/jd9853-module/source/module.cpp create mode 100644 Drivers/st7123-module/CMakeLists.txt create mode 100644 Drivers/st7123-module/LICENSE-Apache-2.0.md create mode 100644 Drivers/st7123-module/README.md create mode 100644 Drivers/st7123-module/bindings/sitronix,st7123-touch.yaml create mode 100644 Drivers/st7123-module/bindings/sitronix,st7123.yaml create mode 100644 Drivers/st7123-module/devicetree.yaml create mode 100644 Drivers/st7123-module/include/bindings/st7123.h create mode 100644 Drivers/st7123-module/include/bindings/st7123_touch.h create mode 100644 Drivers/st7123-module/include/drivers/st7123.h create mode 100644 Drivers/st7123-module/include/drivers/st7123_touch.h create mode 100644 Drivers/st7123-module/include/st7123_module.h create mode 100644 Drivers/st7123-module/source/module.cpp create mode 100644 Drivers/st7123-module/source/st7123.cpp create mode 100644 Drivers/st7123-module/source/st7123_touch.cpp create mode 100644 Drivers/st7796-i8080-module/CMakeLists.txt create mode 100644 Drivers/st7796-i8080-module/LICENSE-Apache-2.0.md create mode 100644 Drivers/st7796-i8080-module/README.md create mode 100644 Drivers/st7796-i8080-module/bindings/sitronix,st7796-i8080.yaml create mode 100644 Drivers/st7796-i8080-module/devicetree.yaml create mode 100644 Drivers/st7796-i8080-module/include/bindings/st7796_i8080.h create mode 100644 Drivers/st7796-i8080-module/include/drivers/st7796_i8080.h create mode 100644 Drivers/st7796-i8080-module/include/st7796_i8080_module.h create mode 100644 Drivers/st7796-i8080-module/source/module.cpp create mode 100644 Drivers/st7796-i8080-module/source/st7796_i8080.cpp diff --git a/Devices/btt-panda-touch/source/module.cpp b/Devices/btt-panda-touch/source/module.cpp index 1471676b7..052e86b99 100644 --- a/Devices/btt-panda-touch/source/module.cpp +++ b/Devices/btt-panda-touch/source/module.cpp @@ -1,19 +1,12 @@ +#include #include extern "C" { -static error_t start() { - return ERROR_NONE; -} - -static error_t stop() { - return ERROR_NONE; -} - struct Module btt_panda_touch_module = { .name = "btt-panda-touch", - .start = start, - .stop = stop, + .start = [] -> error_t { return ERROR_NONE; }, + .stop = [] -> error_t { return ERROR_NONE; }, .symbols = nullptr, .internal = nullptr }; diff --git a/Devices/cyd-2432s024c/source/module.cpp b/Devices/cyd-2432s024c/source/module.cpp index e05e61583..e158f9a37 100644 --- a/Devices/cyd-2432s024c/source/module.cpp +++ b/Devices/cyd-2432s024c/source/module.cpp @@ -1,19 +1,12 @@ +#include #include extern "C" { -static error_t start() { - return ERROR_NONE; -} - -static error_t stop() { - return ERROR_NONE; -} - Module cyd_2432s024c_module = { .name = "cyd-2432s024c", - .start = start, - .stop = stop, + .start = [] -> error_t { return ERROR_NONE; }, + .stop = [] -> error_t { return ERROR_NONE; }, .symbols = nullptr, .internal = nullptr }; diff --git a/Devices/cyd-2432s024r/source/module.cpp b/Devices/cyd-2432s024r/source/module.cpp index 3b2590086..6384f70bc 100644 --- a/Devices/cyd-2432s024r/source/module.cpp +++ b/Devices/cyd-2432s024r/source/module.cpp @@ -1,19 +1,12 @@ +#include #include extern "C" { -static error_t start() { - return ERROR_NONE; -} - -static error_t stop() { - return ERROR_NONE; -} - struct Module cyd_2432s024r_module = { .name = "cyd-2432s024r", - .start = start, - .stop = stop, + .start = [] -> error_t { return ERROR_NONE; }, + .stop = [] -> error_t { return ERROR_NONE; }, .symbols = nullptr, .internal = nullptr }; diff --git a/Devices/cyd-2432s028r/source/module.cpp b/Devices/cyd-2432s028r/source/module.cpp index 26ff57688..4c5c41b85 100644 --- a/Devices/cyd-2432s028r/source/module.cpp +++ b/Devices/cyd-2432s028r/source/module.cpp @@ -1,19 +1,12 @@ +#include #include extern "C" { -static error_t start() { - return ERROR_NONE; -} - -static error_t stop() { - return ERROR_NONE; -} - struct Module cyd_2432s028r_module = { .name = "cyd-2432s028r", - .start = start, - .stop = stop, + .start = [] -> error_t { return ERROR_NONE; }, + .stop = [] -> error_t { return ERROR_NONE; }, .symbols = nullptr, .internal = nullptr }; diff --git a/Devices/cyd-2432s028rv3/source/module.cpp b/Devices/cyd-2432s028rv3/source/module.cpp index ed7f9255e..1ca100b49 100644 --- a/Devices/cyd-2432s028rv3/source/module.cpp +++ b/Devices/cyd-2432s028rv3/source/module.cpp @@ -1,19 +1,12 @@ +#include #include extern "C" { -static error_t start() { - return ERROR_NONE; -} - -static error_t stop() { - return ERROR_NONE; -} - struct Module cyd_2432s028rv3_module = { .name = "cyd-2432s028rv3", - .start = start, - .stop = stop, + .start = [] -> error_t { return ERROR_NONE; }, + .stop = [] -> error_t { return ERROR_NONE; }, .symbols = nullptr, .internal = nullptr }; diff --git a/Devices/cyd-2432s032c/source/module.cpp b/Devices/cyd-2432s032c/source/module.cpp index ee8c6422b..c19d0845d 100644 --- a/Devices/cyd-2432s032c/source/module.cpp +++ b/Devices/cyd-2432s032c/source/module.cpp @@ -1,19 +1,12 @@ +#include #include extern "C" { -static error_t start() { - return ERROR_NONE; -} - -static error_t stop() { - return ERROR_NONE; -} - -struct Module cyd_2432s032c_module = { +Module cyd_2432s032c_module = { .name = "cyd-2432s032c", - .start = start, - .stop = stop, + .start = [] -> error_t { return ERROR_NONE; }, + .stop = [] -> error_t { return ERROR_NONE; }, .symbols = nullptr, .internal = nullptr }; diff --git a/Devices/cyd-3248s035c/source/module.cpp b/Devices/cyd-3248s035c/source/module.cpp index 6c5b2cd9c..9103ae2f1 100644 --- a/Devices/cyd-3248s035c/source/module.cpp +++ b/Devices/cyd-3248s035c/source/module.cpp @@ -1,19 +1,12 @@ +#include #include extern "C" { -static error_t start() { - return ERROR_NONE; -} - -static error_t stop() { - return ERROR_NONE; -} - -struct Module cyd_3248s035c_module = { +Module cyd_3248s035c_module = { .name = "cyd-3248s035c", - .start = start, - .stop = stop, + .start = [] -> error_t { return ERROR_NONE; }, + .stop = [] -> error_t { return ERROR_NONE; }, .symbols = nullptr, .internal = nullptr }; diff --git a/Devices/cyd-4848s040c/source/module.cpp b/Devices/cyd-4848s040c/source/module.cpp index d30b725e3..d3ef66280 100644 --- a/Devices/cyd-4848s040c/source/module.cpp +++ b/Devices/cyd-4848s040c/source/module.cpp @@ -1,19 +1,12 @@ +#include #include extern "C" { -static error_t start() { - return ERROR_NONE; -} - -static error_t stop() { - return ERROR_NONE; -} - -struct Module cyd_4848s040c_module = { +Module cyd_4848s040c_module = { .name = "cyd-4848s040c", - .start = start, - .stop = stop, + .start = [] -> error_t { return ERROR_NONE; }, + .stop = [] -> error_t { return ERROR_NONE; }, .symbols = nullptr, .internal = nullptr }; diff --git a/Devices/cyd-8048s043c/source/module.cpp b/Devices/cyd-8048s043c/source/module.cpp index d638238dc..366490834 100644 --- a/Devices/cyd-8048s043c/source/module.cpp +++ b/Devices/cyd-8048s043c/source/module.cpp @@ -1,19 +1,12 @@ +#include #include extern "C" { -static error_t start() { - return ERROR_NONE; -} - -static error_t stop() { - return ERROR_NONE; -} - -struct Module cyd_8048s043c_module = { +Module cyd_8048s043c_module = { .name = "cyd-8048s043c", - .start = start, - .stop = stop, + .start = [] -> error_t { return ERROR_NONE; }, + .stop = [] -> error_t { return ERROR_NONE; }, .symbols = nullptr, .internal = nullptr }; diff --git a/Devices/cyd-e32r28t/source/module.cpp b/Devices/cyd-e32r28t/source/module.cpp index 3ee0556ca..a8eaf1cb1 100644 --- a/Devices/cyd-e32r28t/source/module.cpp +++ b/Devices/cyd-e32r28t/source/module.cpp @@ -1,19 +1,12 @@ +#include #include extern "C" { -static error_t start() { - return ERROR_NONE; -} - -static error_t stop() { - return ERROR_NONE; -} - -struct Module cyd_e32r28t_module = { +Module cyd_e32r28t_module = { .name = "cyd-e32r28t", - .start = start, - .stop = stop, + .start = [] -> error_t { return ERROR_NONE; }, + .stop = [] -> error_t { return ERROR_NONE; }, .symbols = nullptr, .internal = nullptr }; diff --git a/Devices/cyd-e32r32p/source/module.cpp b/Devices/cyd-e32r32p/source/module.cpp index 1a3df3034..c6936a064 100644 --- a/Devices/cyd-e32r32p/source/module.cpp +++ b/Devices/cyd-e32r32p/source/module.cpp @@ -1,19 +1,12 @@ +#include #include extern "C" { -static error_t start() { - return ERROR_NONE; -} - -static error_t stop() { - return ERROR_NONE; -} - -struct Module cyd_e32r32p_module = { +Module cyd_e32r32p_module = { .name = "cyd-e32r32p", - .start = start, - .stop = stop, + .start = [] -> error_t { return ERROR_NONE; }, + .stop = [] -> error_t { return ERROR_NONE; }, .symbols = nullptr, .internal = nullptr }; diff --git a/Devices/elecrow-crowpanel-advance-28/source/module.cpp b/Devices/elecrow-crowpanel-advance-28/source/module.cpp index df8623120..5db3093d0 100644 --- a/Devices/elecrow-crowpanel-advance-28/source/module.cpp +++ b/Devices/elecrow-crowpanel-advance-28/source/module.cpp @@ -1,19 +1,12 @@ +#include #include extern "C" { -static error_t start() { - return ERROR_NONE; -} - -static error_t stop() { - return ERROR_NONE; -} - -struct Module elecrow_crowpanel_advance_28_module = { +Module elecrow_crowpanel_advance_28_module = { .name = "elecrow-crowpanel-advance-28", - .start = start, - .stop = stop, + .start = [] -> error_t { return ERROR_NONE; }, + .stop = [] -> error_t { return ERROR_NONE; }, .symbols = nullptr }; diff --git a/Devices/elecrow-crowpanel-advance-35/source/module.cpp b/Devices/elecrow-crowpanel-advance-35/source/module.cpp index 829e89181..e70e4928d 100644 --- a/Devices/elecrow-crowpanel-advance-35/source/module.cpp +++ b/Devices/elecrow-crowpanel-advance-35/source/module.cpp @@ -1,19 +1,12 @@ +#include #include extern "C" { -static error_t start() { - return ERROR_NONE; -} - -static error_t stop() { - return ERROR_NONE; -} - Module elecrow_crowpanel_advance_35_module = { .name = "elecrow-crowpanel-advance-35", - .start = start, - .stop = stop, + .start = [] -> error_t { return ERROR_NONE; }, + .stop = [] -> error_t { return ERROR_NONE; }, .symbols = nullptr, .internal = nullptr }; diff --git a/Devices/elecrow-crowpanel-advance-50/source/module.cpp b/Devices/elecrow-crowpanel-advance-50/source/module.cpp index 392ea2e54..e9e8077a0 100644 --- a/Devices/elecrow-crowpanel-advance-50/source/module.cpp +++ b/Devices/elecrow-crowpanel-advance-50/source/module.cpp @@ -1,19 +1,12 @@ +#include #include extern "C" { -static error_t start() { - return ERROR_NONE; -} - -static error_t stop() { - return ERROR_NONE; -} - -struct Module elecrow_crowpanel_advance_50_module = { +Module elecrow_crowpanel_advance_50_module = { .name = "elecrow-crowpanel-advance-50", - .start = start, - .stop = stop, + .start = [] -> error_t { return ERROR_NONE; }, + .stop = [] -> error_t { return ERROR_NONE; }, .symbols = nullptr, .internal = nullptr }; diff --git a/Devices/elecrow-crowpanel-basic-28/source/module.cpp b/Devices/elecrow-crowpanel-basic-28/source/module.cpp index 4f792da31..baa050aba 100644 --- a/Devices/elecrow-crowpanel-basic-28/source/module.cpp +++ b/Devices/elecrow-crowpanel-basic-28/source/module.cpp @@ -1,19 +1,12 @@ +#include #include extern "C" { -static error_t start() { - return ERROR_NONE; -} - -static error_t stop() { - return ERROR_NONE; -} - -struct Module elecrow_crowpanel_basic_28_module = { +Module elecrow_crowpanel_basic_28_module = { .name = "elecrow-crowpanel-basic-28", - .start = start, - .stop = stop, + .start = [] -> error_t { return ERROR_NONE; }, + .stop = [] -> error_t { return ERROR_NONE; }, .symbols = nullptr, .internal = nullptr }; diff --git a/Devices/elecrow-crowpanel-basic-35/source/module.cpp b/Devices/elecrow-crowpanel-basic-35/source/module.cpp index f7c18653c..0b6605872 100644 --- a/Devices/elecrow-crowpanel-basic-35/source/module.cpp +++ b/Devices/elecrow-crowpanel-basic-35/source/module.cpp @@ -1,19 +1,12 @@ +#include #include extern "C" { -static error_t start() { - return ERROR_NONE; -} - -static error_t stop() { - return ERROR_NONE; -} - Module elecrow_crowpanel_basic_35_module = { .name = "elecrow-crowpanel-basic-35", - .start = start, - .stop = stop, + .start = [] -> error_t { return ERROR_NONE; }, + .stop = [] -> error_t { return ERROR_NONE; }, .symbols = nullptr, .internal = nullptr }; diff --git a/Devices/elecrow-crowpanel-basic-50/source/module.cpp b/Devices/elecrow-crowpanel-basic-50/source/module.cpp index 392d7cbac..a678c7272 100644 --- a/Devices/elecrow-crowpanel-basic-50/source/module.cpp +++ b/Devices/elecrow-crowpanel-basic-50/source/module.cpp @@ -1,19 +1,12 @@ +#include #include extern "C" { -static error_t start() { - return ERROR_NONE; -} - -static error_t stop() { - return ERROR_NONE; -} - -struct Module elecrow_crowpanel_basic_50_module = { +Module elecrow_crowpanel_basic_50_module = { .name = "elecrow-crowpanel-basic-50", - .start = start, - .stop = stop, + .start = [] -> error_t { return ERROR_NONE; }, + .stop = [] -> error_t { return ERROR_NONE; }, .symbols = nullptr, .internal = nullptr }; diff --git a/Devices/es3c28p/source/module.cpp b/Devices/es3c28p/source/module.cpp index f22143a89..431983863 100644 --- a/Devices/es3c28p/source/module.cpp +++ b/Devices/es3c28p/source/module.cpp @@ -1,19 +1,12 @@ +#include #include extern "C" { -static error_t start() { - return ERROR_NONE; -} - -static error_t stop() { - return ERROR_NONE; -} - -struct Module es3c28p_module = { +Module es3c28p_module = { .name = "es3c28p", - .start = start, - .stop = stop, + .start = [] -> error_t { return ERROR_NONE; }, + .stop = [] -> error_t { return ERROR_NONE; }, .symbols = nullptr, .internal = nullptr }; diff --git a/Devices/generic-esp32/source/module.cpp b/Devices/generic-esp32/source/module.cpp index 70fa7604b..8d1f99746 100644 --- a/Devices/generic-esp32/source/module.cpp +++ b/Devices/generic-esp32/source/module.cpp @@ -1,19 +1,12 @@ +#include #include extern "C" { -static error_t start() { - return ERROR_NONE; -} - -static error_t stop() { - return ERROR_NONE; -} - -struct Module generic_esp32_module = { +Module generic_esp32_module = { .name = "generic-esp32", - .start = start, - .stop = stop, + .start = [] -> error_t { return ERROR_NONE; }, + .stop = [] -> error_t { return ERROR_NONE; }, .symbols = nullptr, .internal = nullptr }; diff --git a/Devices/generic-esp32c6/source/module.cpp b/Devices/generic-esp32c6/source/module.cpp index 2113d5cc9..64a3a0225 100644 --- a/Devices/generic-esp32c6/source/module.cpp +++ b/Devices/generic-esp32c6/source/module.cpp @@ -1,19 +1,12 @@ +#include #include extern "C" { -static error_t start() { - return ERROR_NONE; -} - -static error_t stop() { - return ERROR_NONE; -} - -struct Module generic_esp32c6_module = { +Module generic_esp32c6_module = { .name = "generic-esp32c6", - .start = start, - .stop = stop, + .start = [] -> error_t { return ERROR_NONE; }, + .stop = [] -> error_t { return ERROR_NONE; }, .symbols = nullptr, .internal = nullptr }; diff --git a/Devices/generic-esp32p4/source/module.cpp b/Devices/generic-esp32p4/source/module.cpp index 3a54bde16..8ba83f6e8 100644 --- a/Devices/generic-esp32p4/source/module.cpp +++ b/Devices/generic-esp32p4/source/module.cpp @@ -1,19 +1,12 @@ +#include #include extern "C" { -static error_t start() { - return ERROR_NONE; -} - -static error_t stop() { - return ERROR_NONE; -} - -struct Module generic_esp32p4_module = { +Module generic_esp32p4_module = { .name = "generic-esp32p4", - .start = start, - .stop = stop, + .start = [] -> error_t { return ERROR_NONE; }, + .stop = [] -> error_t { return ERROR_NONE; }, .symbols = nullptr, .internal = nullptr }; diff --git a/Devices/generic-esp32s3/source/module.cpp b/Devices/generic-esp32s3/source/module.cpp index 05fe7d0dc..b27e472d4 100644 --- a/Devices/generic-esp32s3/source/module.cpp +++ b/Devices/generic-esp32s3/source/module.cpp @@ -1,19 +1,12 @@ +#include #include extern "C" { -static error_t start() { - return ERROR_NONE; -} - -static error_t stop() { - return ERROR_NONE; -} - -struct Module generic_esp32s3_module = { +Module generic_esp32s3_module = { .name = "generic-esp32s3", - .start = start, - .stop = stop, + .start = [] -> error_t { return ERROR_NONE; }, + .stop = [] -> error_t { return ERROR_NONE; }, .symbols = nullptr, .internal = nullptr }; diff --git a/Devices/guition-jc1060p470ciwy/source/module.cpp b/Devices/guition-jc1060p470ciwy/source/module.cpp index 789fcff00..727886881 100644 --- a/Devices/guition-jc1060p470ciwy/source/module.cpp +++ b/Devices/guition-jc1060p470ciwy/source/module.cpp @@ -1,19 +1,12 @@ +#include #include extern "C" { -static error_t start() { - return ERROR_NONE; -} - -static error_t stop() { - return ERROR_NONE; -} - Module guition_jc1060p470ciwy_module = { .name = "guition-jc1060p470ciwy", - .start = start, - .stop = stop, + .start = [] -> error_t { return ERROR_NONE; }, + .stop = [] -> error_t { return ERROR_NONE; }, .symbols = nullptr, .internal = nullptr }; diff --git a/Devices/guition-jc2432w328c/source/module.cpp b/Devices/guition-jc2432w328c/source/module.cpp index d1e1fe373..d875db137 100644 --- a/Devices/guition-jc2432w328c/source/module.cpp +++ b/Devices/guition-jc2432w328c/source/module.cpp @@ -1,19 +1,12 @@ +#include #include extern "C" { -static error_t start() { - return ERROR_NONE; -} - -static error_t stop() { - return ERROR_NONE; -} - -struct Module guition_jc2432w328c_module = { +Module guition_jc2432w328c_module = { .name = "guition-jc2432w328c", - .start = start, - .stop = stop, + .start = [] -> error_t { return ERROR_NONE; }, + .stop = [] -> error_t { return ERROR_NONE; }, .symbols = nullptr, .internal = nullptr }; diff --git a/Devices/guition-jc8048w550c/source/module.cpp b/Devices/guition-jc8048w550c/source/module.cpp index 542bf9fa1..34a67026f 100644 --- a/Devices/guition-jc8048w550c/source/module.cpp +++ b/Devices/guition-jc8048w550c/source/module.cpp @@ -1,19 +1,12 @@ +#include #include extern "C" { -static error_t start() { - return ERROR_NONE; -} - -static error_t stop() { - return ERROR_NONE; -} - -struct Module guition_jc8048w550c_module = { +Module guition_jc8048w550c_module = { .name = "guition-jc8048w550c", - .start = start, - .stop = stop, + .start = [] -> error_t { return ERROR_NONE; }, + .stop = [] -> error_t { return ERROR_NONE; }, .symbols = nullptr, .internal = nullptr }; diff --git a/Devices/heltec-wifi-lora-32-v3/source/module.cpp b/Devices/heltec-wifi-lora-32-v3/source/module.cpp index fa497d6f3..52648e29b 100644 --- a/Devices/heltec-wifi-lora-32-v3/source/module.cpp +++ b/Devices/heltec-wifi-lora-32-v3/source/module.cpp @@ -1,19 +1,12 @@ +#include #include extern "C" { -static error_t start() { - return ERROR_NONE; -} - -static error_t stop() { - return ERROR_NONE; -} - -struct Module heltec_wifi_lora_32_v3_module = { +Module heltec_wifi_lora_32_v3_module = { .name = "heltec-wifi-lora-32-v3", - .start = start, - .stop = stop, + .start = [] -> error_t { return ERROR_NONE; }, + .stop = [] -> error_t { return ERROR_NONE; }, .symbols = nullptr, .internal = nullptr }; diff --git a/Devices/m5stack-stackchan/Source/module.cpp b/Devices/m5stack-stackchan/Source/module.cpp index 0441dba2d..ec29f2cc6 100644 --- a/Devices/m5stack-stackchan/Source/module.cpp +++ b/Devices/m5stack-stackchan/Source/module.cpp @@ -1,19 +1,12 @@ +#include #include extern "C" { -static error_t start() { - return ERROR_NONE; -} - -static error_t stop() { - return ERROR_NONE; -} - -struct Module m5stack_stackchan_module = { +Module m5stack_stackchan_module = { .name = "m5stack-stackchan", - .start = start, - .stop = stop, + .start = [] -> error_t { return ERROR_NONE; }, + .stop = [] -> error_t { return ERROR_NONE; }, .symbols = nullptr, .internal = nullptr }; diff --git a/Devices/m5stack-sticks3/CMakeLists.txt b/Devices/m5stack-sticks3/CMakeLists.txt index fbd5c16c6..09a2fdee3 100644 --- a/Devices/m5stack-sticks3/CMakeLists.txt +++ b/Devices/m5stack-sticks3/CMakeLists.txt @@ -1,7 +1,7 @@ -file(GLOB_RECURSE SOURCE_FILES Source/*.c*) +file(GLOB_RECURSE SOURCE_FILES source/*.c*) idf_component_register( SRCS ${SOURCE_FILES} - INCLUDE_DIRS "Source" - REQUIRES Tactility esp_lvgl_port esp_lcd ST7789 PwmBacklight ButtonControl vfs fatfs m5pm1-module + INCLUDE_DIRS "source" + REQUIRES TactilityKernel ) diff --git a/Devices/m5stack-sticks3/Source/Configuration.cpp b/Devices/m5stack-sticks3/Source/Configuration.cpp deleted file mode 100644 index 7a0558770..000000000 --- a/Devices/m5stack-sticks3/Source/Configuration.cpp +++ /dev/null @@ -1,48 +0,0 @@ -#include "devices/Display.h" -#include "devices/Power.h" - -#include -#include - -#include -#include -#include - -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 -}; diff --git a/Devices/m5stack-sticks3/Source/devices/Display.cpp b/Devices/m5stack-sticks3/Source/devices/Display.cpp deleted file mode 100644 index 5b732b068..000000000 --- a/Devices/m5stack-sticks3/Source/devices/Display.cpp +++ /dev/null @@ -1,32 +0,0 @@ -#include "Display.h" - -#include -#include - -std::shared_ptr createDisplay() { - St7789Display::Configuration panel_configuration = { - .horizontalResolution = LCD_HORIZONTAL_RESOLUTION, - .verticalResolution = LCD_VERTICAL_RESOLUTION, - .gapX = 52, - .gapY = 40, - .swapXY = false, - .mirrorX = false, - .mirrorY = false, - .invertColor = true, - .bufferSize = LCD_BUFFER_SIZE, - .touch = nullptr, - .backlightDutyFunction = driver::pwmbacklight::setBacklightDuty, - .resetPin = LCD_PIN_RESET, - .lvglSwapBytes = false - }; - - auto spi_configuration = std::make_shared(St7789Display::SpiConfiguration { - .spiHostDevice = LCD_SPI_HOST, - .csPin = LCD_PIN_CS, - .dcPin = LCD_PIN_DC, - .pixelClockFrequency = 40'000'000, - .transactionQueueDepth = 10 - }); - - return std::make_shared(panel_configuration, spi_configuration); -} diff --git a/Devices/m5stack-sticks3/Source/devices/Display.h b/Devices/m5stack-sticks3/Source/devices/Display.h deleted file mode 100644 index 65142cb5f..000000000 --- a/Devices/m5stack-sticks3/Source/devices/Display.h +++ /dev/null @@ -1,17 +0,0 @@ -#pragma once - -#include "Tactility/hal/display/DisplayDevice.h" -#include -#include -#include - -constexpr auto LCD_SPI_HOST = SPI2_HOST; -constexpr auto LCD_PIN_CS = GPIO_NUM_41; -constexpr auto LCD_PIN_DC = GPIO_NUM_45; -constexpr auto LCD_PIN_RESET = GPIO_NUM_21; -constexpr auto LCD_HORIZONTAL_RESOLUTION = 135; -constexpr auto LCD_VERTICAL_RESOLUTION = 240; -constexpr auto LCD_BUFFER_HEIGHT = LCD_VERTICAL_RESOLUTION / 3; -constexpr auto LCD_BUFFER_SIZE = LCD_HORIZONTAL_RESOLUTION * LCD_BUFFER_HEIGHT; - -std::shared_ptr createDisplay(); diff --git a/Devices/m5stack-sticks3/Source/devices/Power.cpp b/Devices/m5stack-sticks3/Source/devices/Power.cpp deleted file mode 100644 index 036b7595f..000000000 --- a/Devices/m5stack-sticks3/Source/devices/Power.cpp +++ /dev/null @@ -1,94 +0,0 @@ -#include "Power.h" - -#include -#include -#include -#include - -using namespace tt::hal::power; - -static constexpr auto* TAG = "StickS3Power"; - -static constexpr float MIN_BATTERY_VOLTAGE_MV = 3300.0f; -static constexpr float MAX_BATTERY_VOLTAGE_MV = 4200.0f; - -class StickS3Power final : public PowerDevice { -public: - explicit StickS3Power(::Device* m5pm1Device) : m5pm1(m5pm1Device) {} - - std::string getName() const override { return "M5Stack StickS3 Power"; } - std::string getDescription() const override { return "Battery monitoring via M5PM1 over I2C"; } - - bool supportsMetric(MetricType type) const override { - switch (type) { - using enum MetricType; - case BatteryVoltage: - case ChargeLevel: - case IsCharging: - return true; - default: - return false; - } - } - - bool getMetric(MetricType type, MetricData& data) override { - switch (type) { - using enum MetricType; - - case BatteryVoltage: { - uint16_t mv = 0; - if (m5pm1_get_battery_voltage(m5pm1, &mv) != ERROR_NONE) return false; - data.valueAsUint32 = mv; - return true; - } - - case ChargeLevel: { - uint16_t mv = 0; - if (m5pm1_get_battery_voltage(m5pm1, &mv) != ERROR_NONE) return false; - float voltage = static_cast(mv); - if (voltage >= MAX_BATTERY_VOLTAGE_MV) { - data.valueAsUint8 = 100; - } else if (voltage <= MIN_BATTERY_VOLTAGE_MV) { - data.valueAsUint8 = 0; - } else { - float factor = (voltage - MIN_BATTERY_VOLTAGE_MV) / (MAX_BATTERY_VOLTAGE_MV - MIN_BATTERY_VOLTAGE_MV); - data.valueAsUint8 = static_cast(factor * 100.0f); - } - return true; - } - - case IsCharging: { - bool charging = false; - if (m5pm1_is_charging(m5pm1, &charging) != ERROR_NONE) { - LOG_W(TAG, "Failed to read charging status"); - return false; - } - data.valueAsBool = charging; - return true; - } - - default: - return false; - } - } - - bool supportsPowerOff() const override { return true; } - - void powerOff() override { - LOG_W(TAG, "Powering off via M5PM1"); - if (m5pm1_shutdown(m5pm1) != ERROR_NONE) { - LOG_E(TAG, "Failed to send power-off command"); - } - } - -private: - ::Device* m5pm1; -}; - -std::shared_ptr createPower() { - auto* m5pm1 = device_find_by_name("m5pm1"); - if (m5pm1 == nullptr) { - LOG_E(TAG, "m5pm1 device not found"); - } - return std::make_shared(m5pm1); -} diff --git a/Devices/m5stack-sticks3/Source/devices/Power.h b/Devices/m5stack-sticks3/Source/devices/Power.h deleted file mode 100644 index 7598ded6f..000000000 --- a/Devices/m5stack-sticks3/Source/devices/Power.h +++ /dev/null @@ -1,6 +0,0 @@ -#pragma once - -#include -#include - -std::shared_ptr createPower(); diff --git a/Devices/m5stack-sticks3/Source/module.cpp b/Devices/m5stack-sticks3/Source/module.cpp deleted file mode 100644 index 77b30f3c2..000000000 --- a/Devices/m5stack-sticks3/Source/module.cpp +++ /dev/null @@ -1,23 +0,0 @@ -#include - -extern "C" { - -static error_t start() { - // Empty for now - return ERROR_NONE; -} - -static error_t stop() { - // Empty for now - return ERROR_NONE; -} - -struct Module m5stack_sticks3_module = { - .name = "m5stack-sticks3", - .start = start, - .stop = stop, - .symbols = nullptr, - .internal = nullptr -}; - -} diff --git a/Devices/m5stack-sticks3/device.properties b/Devices/m5stack-sticks3/device.properties index 057115eca..ac16de421 100644 --- a/Devices/m5stack-sticks3/device.properties +++ b/Devices/m5stack-sticks3/device.properties @@ -12,6 +12,8 @@ hardware.esptoolFlashFreq=80M hardware.tinyUsb=true hardware.bluetooth=true +dependencies.useDeprecatedHal=false + storage.userDataLocation=Internal display.size=1.14" diff --git a/Devices/m5stack-sticks3/devicetree.yaml b/Devices/m5stack-sticks3/devicetree.yaml index fed26664f..90a765793 100644 --- a/Devices/m5stack-sticks3/devicetree.yaml +++ b/Devices/m5stack-sticks3/devicetree.yaml @@ -4,4 +4,6 @@ dependencies: - Drivers/m5pm1-module - Drivers/es8311-module - Drivers/audio-stream-module + - Drivers/st7789-module + - Drivers/button-control-module dts: m5stack,sticks3.dts diff --git a/Devices/m5stack-sticks3/m5stack,sticks3.dts b/Devices/m5stack-sticks3/m5stack,sticks3.dts index f5a1c17da..cbc4fa30f 100644 --- a/Devices/m5stack-sticks3/m5stack,sticks3.dts +++ b/Devices/m5stack-sticks3/m5stack,sticks3.dts @@ -11,7 +11,11 @@ #include #include #include -#include +#include +#include + +#include +#include / { compatible = "root"; @@ -53,6 +57,8 @@ m5pm1 { compatible = "m5stack,m5pm1"; reg = <0x6E>; + power-supply-reference-voltage-mv = <4200>; + speaker-amp-enable-at-boot; }; bmi270 { @@ -75,18 +81,49 @@ 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 { compatible = "espressif,esp32-spi"; host = ; cs-gpios = <&gpio0 41 GPIO_FLAG_NONE>; pin-mosi = <&gpio0 39 GPIO_FLAG_NONE>; pin-sclk = <&gpio0 40 GPIO_FLAG_NONE>; - - display { - compatible = "display-placeholder"; + + display@0 { + 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 { compatible = "espressif,esp32-uart"; status = "disabled"; diff --git a/Devices/m5stack-sticks3/source/module.cpp b/Devices/m5stack-sticks3/source/module.cpp new file mode 100644 index 000000000..5592c4a4f --- /dev/null +++ b/Devices/m5stack-sticks3/source/module.cpp @@ -0,0 +1,14 @@ +#include +#include + +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 +}; + +} diff --git a/Devices/m5stack-tab5/CMakeLists.txt b/Devices/m5stack-tab5/CMakeLists.txt index ed36355cf..61450f7bb 100644 --- a/Devices/m5stack-tab5/CMakeLists.txt +++ b/Devices/m5stack-tab5/CMakeLists.txt @@ -3,5 +3,5 @@ file(GLOB_RECURSE SOURCE_FILES Source/*.c*) idf_component_register( SRCS ${SOURCE_FILES} INCLUDE_DIRS "Source" - REQUIRES Tactility esp_lvgl_port esp_lcd EspLcdCompat esp_lcd_ili9881c 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 ) diff --git a/Devices/m5stack-tab5/Source/Configuration.cpp b/Devices/m5stack-tab5/Source/Configuration.cpp deleted file mode 100644 index 561fd198d..000000000 --- a/Devices/m5stack-tab5/Source/Configuration.cpp +++ /dev/null @@ -1,205 +0,0 @@ -#include "devices/Display.h" -#include "devices/Power.h" -#include "devices/Tab5Keyboard.h" - -#include -#include - -#include - -#include - -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(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 -}; diff --git a/Devices/m5stack-tab5/Source/devices/Detect.cpp b/Devices/m5stack-tab5/Source/devices/Detect.cpp deleted file mode 100644 index 037036576..000000000 --- a/Devices/m5stack-tab5/Source/devices/Detect.cpp +++ /dev/null @@ -1,45 +0,0 @@ -#include "Detect.h" - -#include -#include -#include -#include -#include -#include -#include - -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; -} diff --git a/Devices/m5stack-tab5/Source/devices/Detect.h b/Devices/m5stack-tab5/Source/devices/Detect.h deleted file mode 100644 index 96d159f45..000000000 --- a/Devices/m5stack-tab5/Source/devices/Detect.h +++ /dev/null @@ -1,8 +0,0 @@ -#pragma once - -enum class Tab5Variant { - Ili9881c_Gt911, // Older variant - St7123, // Newer variant (default) -}; - -[[nodiscard]] Tab5Variant detectVariant(); diff --git a/Devices/m5stack-tab5/Source/devices/Display.cpp b/Devices/m5stack-tab5/Source/devices/Display.cpp deleted file mode 100644 index a2d8ef50a..000000000 --- a/Devices/m5stack-tab5/Source/devices/Display.cpp +++ /dev/null @@ -1,108 +0,0 @@ -#include "Detect.h" -#include "Display.h" -#include "Ili9881cDisplay.h" -#include "St7123Display.h" -#include "St7123Touch.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -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 createGt911Touch() { - auto* i2c = device_find_by_name("i2c0"); - check(i2c); - auto configuration = std::make_unique( - 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(std::move(configuration)); -} - -static std::shared_ptr createSt7123Touch() { - auto* i2c = device_find_by_name("i2c0"); - check(i2c, "i2c0 not found"); - auto configuration = std::make_unique( - i2c, - 720, - 1280, - false, // swapXY - false, // mirrorX - false, // mirrorY - GPIO_NUM_23 // interrupt pin - ); - return std::make_shared(std::move(configuration)); -} - -std::shared_ptr createDisplay() { - // Initialize PWM backlight - if (!driver::pwmbacklight::init(LCD_PIN_BACKLIGHT, 5000, LEDC_TIMER_1, LEDC_CHANNEL_0)) { - LOG_W(TAG, "Failed to initialize backlight"); - } - - Tab5Variant variant = detectVariant(); - - std::shared_ptr 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 { - .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( - std::make_shared(configuration) - ); - } else { - return std::static_pointer_cast( - std::make_shared(configuration) - ); - } -} diff --git a/Devices/m5stack-tab5/Source/devices/Display.h b/Devices/m5stack-tab5/Source/devices/Display.h deleted file mode 100644 index 344191d68..000000000 --- a/Devices/m5stack-tab5/Source/devices/Display.h +++ /dev/null @@ -1,6 +0,0 @@ -#pragma once - -#include -#include - -std::shared_ptr createDisplay(); diff --git a/Devices/m5stack-tab5/Source/devices/Ili9881cDisplay.cpp b/Devices/m5stack-tab5/Source/devices/Ili9881cDisplay.cpp deleted file mode 100644 index aec496bd2..000000000 --- a/Devices/m5stack-tab5/Source/devices/Ili9881cDisplay.cpp +++ /dev/null @@ -1,154 +0,0 @@ -#include "Ili9881cDisplay.h" -#include "ili9881_init_data.h" - -#include -#include - -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, 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(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, - }, - }; -} diff --git a/Devices/m5stack-tab5/Source/devices/Ili9881cDisplay.h b/Devices/m5stack-tab5/Source/devices/Ili9881cDisplay.h deleted file mode 100644 index 3643df0dd..000000000 --- a/Devices/m5stack-tab5/Source/devices/Ili9881cDisplay.h +++ /dev/null @@ -1,38 +0,0 @@ -#pragma once - -#include - -#include -#include - -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, 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& configuration - ) : EspLcdDisplayV2(configuration) {} - - ~Ili9881cDisplay() override; - - std::string getName() const override { return "ILI9881C"; } - - std::string getDescription() const override { return "ILI9881C MIPI-DSI display"; } -}; diff --git a/Devices/m5stack-tab5/Source/devices/Power.cpp b/Devices/m5stack-tab5/Source/devices/Power.cpp deleted file mode 100644 index 1cc9f6b1f..000000000 --- a/Devices/m5stack-tab5/Source/devices/Power.cpp +++ /dev/null @@ -1,192 +0,0 @@ -#include "Power.h" - -#include -#include -#include -#include -#include -#include -#include - -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(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(factor * 100.0f); - } - return true; - } - - case Current: { - if (ina226 == nullptr) return false; - float amps = 0.0f; - if (ina226_read_shunt_current(ina226, &s) != 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(-amps * 1000.0f); - return true; - } - - case IsCharging: { - if (ina226 == nullptr) return false; - float amps = 0.0f; - if (ina226_read_shunt_current(ina226, &s) != 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 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(ina226, io_expander1); -} diff --git a/Devices/m5stack-tab5/Source/devices/Power.h b/Devices/m5stack-tab5/Source/devices/Power.h deleted file mode 100644 index 7598ded6f..000000000 --- a/Devices/m5stack-tab5/Source/devices/Power.h +++ /dev/null @@ -1,6 +0,0 @@ -#pragma once - -#include -#include - -std::shared_ptr createPower(); diff --git a/Devices/m5stack-tab5/Source/devices/St7123Display.cpp b/Devices/m5stack-tab5/Source/devices/St7123Display.cpp deleted file mode 100644 index 0f47e1252..000000000 --- a/Devices/m5stack-tab5/Source/devices/St7123Display.cpp +++ /dev/null @@ -1,148 +0,0 @@ -#include "St7123Display.h" -#include "st7123_init_data.h" - -#include -#include - -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, 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, - }, - }; -} diff --git a/Devices/m5stack-tab5/Source/devices/St7123Display.h b/Devices/m5stack-tab5/Source/devices/St7123Display.h deleted file mode 100644 index adb9a28ab..000000000 --- a/Devices/m5stack-tab5/Source/devices/St7123Display.h +++ /dev/null @@ -1,38 +0,0 @@ -#pragma once - -#include - -#include -#include - -class St7123Display final : public EspLcdDisplayV2 { - - esp_lcd_dsi_bus_handle_t mipiDsiBus = nullptr; - esp_ldo_channel_handle_t ldoChannel = nullptr; - - bool createMipiDsiBus(); - -protected: - - bool createIoHandle(esp_lcd_panel_io_handle_t& ioHandle) override; - - esp_lcd_panel_dev_config_t createPanelConfig(std::shared_ptr espLcdConfiguration, gpio_num_t resetPin) override; - - bool createPanelHandle(esp_lcd_panel_io_handle_t ioHandle, const esp_lcd_panel_dev_config_t& panelConfig, esp_lcd_panel_handle_t& panelHandle) override; - - bool useDsiPanel() const override { return true; } - - lvgl_port_display_dsi_cfg_t getLvglPortDisplayDsiConfig(esp_lcd_panel_io_handle_t /*ioHandle*/, esp_lcd_panel_handle_t /*panelHandle*/) override; - -public: - - St7123Display( - const std::shared_ptr& configuration - ) : EspLcdDisplayV2(configuration) {} - - ~St7123Display() override; - - std::string getName() const override { return "St7123"; } - - std::string getDescription() const override { return "St7123 MIPI-DSI display"; } -}; diff --git a/Devices/m5stack-tab5/Source/devices/St7123Touch.cpp b/Devices/m5stack-tab5/Source/devices/St7123Touch.cpp deleted file mode 100644 index 4492e77c9..000000000 --- a/Devices/m5stack-tab5/Source/devices/St7123Touch.cpp +++ /dev/null @@ -1,41 +0,0 @@ -#include "St7123Touch.h" - -#include -#include -#include -#include - -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 - }; -} diff --git a/Devices/m5stack-tab5/Source/devices/St7123Touch.h b/Devices/m5stack-tab5/Source/devices/St7123Touch.h deleted file mode 100644 index ec483cccc..000000000 --- a/Devices/m5stack-tab5/Source/devices/St7123Touch.h +++ /dev/null @@ -1,59 +0,0 @@ -#pragma once - -#include -#include -#include - -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; - - bool createIoHandle(esp_lcd_panel_io_handle_t& outHandle) override; - - bool createTouchHandle(esp_lcd_panel_io_handle_t ioHandle, const esp_lcd_touch_config_t& config, esp_lcd_touch_handle_t& touchHandle) override; - - esp_lcd_touch_config_t createEspLcdTouchConfig() override; - -public: - - explicit St7123Touch(std::unique_ptr inConfiguration) : configuration(std::move(inConfiguration)) { - assert(configuration != nullptr); - } - - std::string getName() const override { return "ST7123Touch"; } - - std::string getDescription() const override { return "ST7123 I2C touch driver"; } -}; diff --git a/Devices/m5stack-tab5/Source/devices/Tab5Keyboard.cpp b/Devices/m5stack-tab5/Source/devices/Tab5Keyboard.cpp deleted file mode 100644 index e5f71f657..000000000 --- a/Devices/m5stack-tab5/Source/devices/Tab5Keyboard.cpp +++ /dev/null @@ -1,561 +0,0 @@ -#include "Tab5Keyboard.h" -#include -#include -#include -#include -#include -#include -#include - -// --------------------------------------------------------------------------- -// 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 a–z / A–Z - if (keycode >= 0x04U && keycode <= 0x1DU) { - uint32_t c = static_cast('a' + (keycode - 0x04U)); - return shift ? (c - 0x20U) : c; - } - - // Numbers 1–0 and their shifted symbols - if (keycode >= 0x1EU && keycode <= 0x27U) { - static constexpr char nums[] = "1234567890"; - static constexpr char snums[] = "!@#$%^&*()"; - return shift ? static_cast(snums[keycode - 0x1EU]) - : static_cast(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(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(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(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(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(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::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; -} diff --git a/Devices/m5stack-tab5/Source/devices/Tab5Keyboard.h b/Devices/m5stack-tab5/Source/devices/Tab5Keyboard.h deleted file mode 100644 index 8a052d235..000000000 --- a/Devices/m5stack-tab5/Source/devices/Tab5Keyboard.h +++ /dev/null @@ -1,85 +0,0 @@ -#pragma once -#include -#include -#include -#include -#include - -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 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(); -}; diff --git a/Devices/m5stack-tab5/Source/devices/detect.cpp b/Devices/m5stack-tab5/Source/devices/detect.cpp new file mode 100644 index 000000000..9e9f36ac3 --- /dev/null +++ b/Devices/m5stack-tab5/Source/devices/detect.cpp @@ -0,0 +1,48 @@ +#include "detect.h" + +#include +#include +#include + +#include +#include + +#include + +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; +} diff --git a/Devices/m5stack-tab5/Source/devices/detect.h b/Devices/m5stack-tab5/Source/devices/detect.h new file mode 100644 index 000000000..ddbf3fb3d --- /dev/null +++ b/Devices/m5stack-tab5/Source/devices/detect.h @@ -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); diff --git a/Devices/m5stack-tab5/Source/devices/devices_common.cpp b/Devices/m5stack-tab5/Source/devices/devices_common.cpp new file mode 100644 index 000000000..f93da7ecd --- /dev/null +++ b/Devices/m5stack-tab5/Source/devices/devices_common.cpp @@ -0,0 +1,45 @@ +#include "devices_common.h" + +#include +#include +#include + +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; +} diff --git a/Devices/m5stack-tab5/Source/devices/devices_common.h b/Devices/m5stack-tab5/Source/devices/devices_common.h new file mode 100644 index 000000000..8cb73468e --- /dev/null +++ b/Devices/m5stack-tab5/Source/devices/devices_common.h @@ -0,0 +1,29 @@ +#pragma once + +#include + +#include +#include +#include + +// 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 +void flatten_init_sequence(const InitCmd* cmds, size_t count, std::vector& out) { + for (size_t i = 0; i < count; i++) { + out.push_back(static_cast(cmds[i].cmd)); + out.push_back(static_cast(cmds[i].data_bytes)); + out.push_back(static_cast(cmds[i].delay_ms)); + const auto* data = static_cast(cmds[i].data); + for (size_t j = 0; j < cmds[i].data_bytes; j++) { + out.push_back(data[j]); + } + } +} diff --git a/Devices/m5stack-tab5/Source/devices/devices_v1.cpp b/Devices/m5stack-tab5/Source/devices/devices_v1.cpp new file mode 100644 index 000000000..07df7c80c --- /dev/null +++ b/Devices/m5stack-tab5/Source/devices/devices_v1.cpp @@ -0,0 +1,145 @@ +#include "devices_v1.h" + +#include "devices_common.h" +#include "ili9881_init_data.h" + +#include +#include +#include +#include + +#include +#include + +#include +#include + +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 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 = >911_config; + + apply_gt911_int_workaround(); + + construct_add_start(>911_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); +} diff --git a/Devices/m5stack-tab5/Source/devices/devices_v1.h b/Devices/m5stack-tab5/Source/devices/devices_v1.h new file mode 100644 index 000000000..37c4c90fd --- /dev/null +++ b/Devices/m5stack-tab5/Source/devices/devices_v1.h @@ -0,0 +1,5 @@ +#pragma once + +struct Device; + +void tab5_create_devices_v1(Device* i2c0); diff --git a/Devices/m5stack-tab5/Source/devices/devices_v2.cpp b/Devices/m5stack-tab5/Source/devices/devices_v2.cpp new file mode 100644 index 000000000..980bc81ad --- /dev/null +++ b/Devices/m5stack-tab5/Source/devices/devices_v2.cpp @@ -0,0 +1,124 @@ +#include "devices_v2.h" + +#include "devices_common.h" +#include "st7123_init_data.h" + +#include +#include +#include + +#include +#include + +#include +#include + +#define TAG "Tab5" + +static std::vector 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); +} diff --git a/Devices/m5stack-tab5/Source/devices/devices_v2.h b/Devices/m5stack-tab5/Source/devices/devices_v2.h new file mode 100644 index 000000000..3d2e5ddd1 --- /dev/null +++ b/Devices/m5stack-tab5/Source/devices/devices_v2.h @@ -0,0 +1,5 @@ +#pragma once + +struct Device; + +void tab5_create_devices_v2(Device* i2c0); diff --git a/Devices/m5stack-tab5/Source/devices/display_detect.cpp b/Devices/m5stack-tab5/Source/devices/display_detect.cpp new file mode 100644 index 000000000..644f0442e --- /dev/null +++ b/Devices/m5stack-tab5/Source/devices/display_detect.cpp @@ -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 +#include +#include +#include + +#include + +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); +} diff --git a/Devices/m5stack-tab5/Source/devices/tab5_headphone_detect.cpp b/Devices/m5stack-tab5/Source/devices/tab5_headphone_detect.cpp new file mode 100644 index 000000000..99a825505 --- /dev/null +++ b/Devices/m5stack-tab5/Source/devices/tab5_headphone_detect.cpp @@ -0,0 +1,109 @@ +#include "tab5_headphone_detect.h" + +#include +#include +#include + +#include +#include + +#include + +#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 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); +} diff --git a/Devices/m5stack-tab5/Source/devices/tab5_headphone_detect.h b/Devices/m5stack-tab5/Source/devices/tab5_headphone_detect.h new file mode 100644 index 000000000..51da6bc4f --- /dev/null +++ b/Devices/m5stack-tab5/Source/devices/tab5_headphone_detect.h @@ -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(); diff --git a/Devices/m5stack-tab5/Source/devices/tab5_keyboard.cpp b/Devices/m5stack-tab5/Source/devices/tab5_keyboard.cpp new file mode 100644 index 000000000..379b3737e --- /dev/null +++ b/Devices/m5stack-tab5/Source/devices/tab5_keyboard.cpp @@ -0,0 +1,695 @@ +// SPDX-License-Identifier: Apache-2.0 +#include "tab5_keyboard.h" + +#include "devices_common.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +#include + +#include +#include + +constexpr auto* TAG = "Tab5Keyboard"; +#define GET_CONFIG(device) (static_cast((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 a–z / A–Z + if (keycode >= 0x04U && keycode <= 0x1DU) { + uint32_t c = static_cast('a' + (keycode - 0x04U)); + return shift ? (c - 0x20U) : c; + } + + // Numbers 1–0 and their shifted symbols + if (keycode >= 0x1EU && keycode <= 0x27U) { + static constexpr char nums[] = "1234567890"; + static constexpr char snums[] = "!@#$%^&*()"; + return shift ? static_cast(snums[keycode - 0x1EU]) + : static_cast(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(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(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(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(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(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(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(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(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(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 diff --git a/Devices/m5stack-tab5/Source/devices/tab5_keyboard.h b/Devices/m5stack-tab5/Source/devices/tab5_keyboard.h new file mode 100644 index 000000000..4194e4997 --- /dev/null +++ b/Devices/m5stack-tab5/Source/devices/tab5_keyboard.h @@ -0,0 +1,46 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include +#include + +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 diff --git a/Devices/m5stack-tab5/Source/devices/tab5_power_control.cpp b/Devices/m5stack-tab5/Source/devices/tab5_power_control.cpp new file mode 100644 index 000000000..306e598c0 --- /dev/null +++ b/Devices/m5stack-tab5/Source/devices/tab5_power_control.cpp @@ -0,0 +1,201 @@ +// SPDX-License-Identifier: Apache-2.0 +#include "tab5_power_control.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +#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(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(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(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(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(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(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 +}; diff --git a/Devices/m5stack-tab5/Source/devices/tab5_power_control.h b/Devices/m5stack-tab5/Source/devices/tab5_power_control.h new file mode 100644 index 000000000..4cb4a8414 --- /dev/null +++ b/Devices/m5stack-tab5/Source/devices/tab5_power_control.h @@ -0,0 +1,19 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +// 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 diff --git a/Devices/m5stack-tab5/Source/devices/tab_5_camera.cpp b/Devices/m5stack-tab5/Source/devices/tab_5_camera.cpp new file mode 100644 index 000000000..fcf90a5e5 --- /dev/null +++ b/Devices/m5stack-tab5/Source/devices/tab_5_camera.cpp @@ -0,0 +1,30 @@ +#include "tab_5_camera.h" + +#include + +#include + +#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)"); +} diff --git a/Devices/m5stack-tab5/Source/devices/tab_5_camera.h b/Devices/m5stack-tab5/Source/devices/tab_5_camera.h new file mode 100644 index 000000000..ba4b1ac3b --- /dev/null +++ b/Devices/m5stack-tab5/Source/devices/tab_5_camera.h @@ -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(); diff --git a/Devices/m5stack-tab5/Source/module.cpp b/Devices/m5stack-tab5/Source/module.cpp index d4d0984d5..db2c79481 100644 --- a/Devices/m5stack-tab5/Source/module.cpp +++ b/Devices/m5stack-tab5/Source/module.cpp @@ -1,178 +1,100 @@ #include +#include #include +#include +#include +#include #include #include -#include +#include -#include "devices/Tab5Keyboard.h" - -#include -#include - -#include +#include "devices/detect.h" +#include "devices/tab5_headphone_detect.h" +#include "devices/tab5_keyboard.h" +#include "devices/tab5_power_control.h" +#include "devices/tab_5_camera.h" #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; -constexpr auto KB_DETECT_POLL_MS = 1000; - -// hp_detect_timer and kb_detect_timer are only touched from start()/stop(), which are called -// serially by the module manager — no atomic needed for the handles themselves. -static TimerHandle_t hp_detect_timer = nullptr; -static TimerHandle_t kb_detect_timer = nullptr; -static std::atomic io_expander0_cached { nullptr }; -// Flags are written by the timer daemon task and read by start()/stop() — use atomics. -static std::atomic hp_detect_last { false }; -static std::atomic hp_detect_initialized { false }; -static std::atomic kb_late_started { false }; - -static void headphoneDetectCallback(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; - } +static void tab5_init_expander0(Device* io_expander0) { + // Speaker and heapdhone pins are managed at runtime, so we can't have them as a hog in the dts file + // We have to init them manually. + auto* speaker_enable_pin = gpio_descriptor_acquire(io_expander0, GPIO_EXP0_PIN_SPEAKER_ENABLE, GPIO_OWNER_GPIO); + check(speaker_enable_pin); + auto* headphone_detect_pin = gpio_descriptor_acquire(io_expander0, GPIO_EXP0_PIN_HEADPHONE_DETECT, GPIO_OWNER_GPIO); + check(headphone_detect_pin); - bool hp = false; - error_t err = gpio_descriptor_get_level(hp_pin, &hp); - gpio_descriptor_release(hp_pin); + gpio_descriptor_set_flags(speaker_enable_pin, GPIO_FLAG_DIRECTION_OUTPUT); + gpio_descriptor_set_flags(headphone_detect_pin, GPIO_FLAG_DIRECTION_INPUT); - if (err != ERROR_NONE) { - LOG_W(TAG, "hp_detect: HP_DET read error: %s", error_to_string(err)); - return; - } + gpio_descriptor_set_level(speaker_enable_pin, false); - 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"); - } + gpio_descriptor_release(speaker_enable_pin); + gpio_descriptor_release(headphone_detect_pin); } -// 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; +static void tab5_enable_speaker_amp(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)); } +} - using namespace tt::hal; - auto keyboard = findFirstDevice(tt::hal::Device::Type::Keyboard); - if (!keyboard) { - return; // Not registered yet, will retry on next tick - } +// 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; - 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); + static bool did_init = false; + if (did_init || event != DEVICE_EVENT_STARTED || strcmp(device->name, "io_expander0") != 0) { return; } + did_init = true; - if (!keyboard->isAttached()) { - return; // Not plugged in yet, will retry on next tick - } - - auto tab5_keyboard = std::static_pointer_cast(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); - } + tab5_init_expander0(device); + tab5_enable_speaker_amp(device); } extern "C" { static error_t start() { - if (hp_detect_timer != nullptr) { - LOG_W(TAG, "hp_detect timer already running"); - return ERROR_NONE; - } + /* Crash when construct fails, because if a single driver fails to construct, + * there is no guarantee that the previously constructed drivers can be destroyed */ + check(driver_construct_add(&tab5_keyboard_driver) == ERROR_NONE); + check(driver_construct_add(&tab5_power_control_driver) == ERROR_NONE); - hp_detect_initialized = false; - hp_detect_last = false; + tab5_detect_start(); - hp_detect_timer = xTimerCreate("hp_detect", pdMS_TO_TICKS(HP_DETECT_POLL_MS), pdTRUE, nullptr, headphoneDetectCallback); - 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; - } + tab5_camera_init(); - 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); - 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; - } + tab5_headphone_detect_start(); return ERROR_NONE; } static error_t stop() { - if (hp_detect_timer != nullptr) { - 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); - } + tab5_headphone_detect_stop(); - if (kb_detect_timer != nullptr) { - if (xTimerStop(kb_detect_timer, pdMS_TO_TICKS(100)) != pdPASS) { - LOG_W(TAG, "Failed to stop kb_detect timer"); - } - if (xTimerDelete(kb_detect_timer, pdMS_TO_TICKS(100)) != pdPASS) { - LOG_E(TAG, "Failed to delete kb_detect timer"); - } - kb_detect_timer = nullptr; - } + device_listener_remove(on_io_expander0_started); + + tab5_detect_stop(); + + /* Crash when destruct fails, because if a single driver fails to destruct, + * there is no guarantee that the previously destroyed drivers can be recovered */ + check(driver_remove_destruct(&tab5_keyboard_driver) == ERROR_NONE); + check(driver_remove_destruct(&tab5_power_control_driver) == ERROR_NONE); return ERROR_NONE; } diff --git a/Devices/m5stack-tab5/device.properties b/Devices/m5stack-tab5/device.properties index 99766dccc..a7a9ca58b 100644 --- a/Devices/m5stack-tab5/device.properties +++ b/Devices/m5stack-tab5/device.properties @@ -15,6 +15,8 @@ hardware.tinyUsb=true storage.userDataLocation=SD +dependencies.useDeprecatedHal=false + display.size=5" display.shape=rectangle display.dpi=294 diff --git a/Devices/m5stack-tab5/devicetree.yaml b/Devices/m5stack-tab5/devicetree.yaml index 08ee968fc..d0b9c83e7 100644 --- a/Devices/m5stack-tab5/devicetree.yaml +++ b/Devices/m5stack-tab5/devicetree.yaml @@ -8,4 +8,7 @@ dependencies: - Drivers/ina226-module - Drivers/rx8130ce-module - Drivers/sc2356-module + - Drivers/ili9881c-module + - Drivers/st7123-module + - Drivers/gt911-module dts: m5stack,tab5.dts diff --git a/Devices/m5stack-tab5/m5stack,tab5.dts b/Devices/m5stack-tab5/m5stack,tab5.dts index 257952b4b..2039d5186 100644 --- a/Devices/m5stack-tab5/m5stack,tab5.dts +++ b/Devices/m5stack-tab5/m5stack,tab5.dts @@ -6,10 +6,13 @@ #include #include #include +#include #include #include #include #include +#include +#include #include #include #include @@ -37,6 +40,19 @@ 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 i2s0 { compatible = "espressif,esp32-i2s"; @@ -55,12 +71,12 @@ pin-sda = <&gpio0 31 GPIO_FLAG_PULL_UP>; pin-scl = <&gpio0 32 GPIO_FLAG_PULL_UP>; - io_expander0 { + io_expander0: io_expander0 { compatible = "diodes,pi4ioe5v6408"; reg = <0x43>; }; - io_expander1 { + io_expander1: io_expander1 { compatible = "diodes,pi4ioe5v6408"; reg = <0x44>; }; @@ -98,6 +114,10 @@ compatible = "ti,ina226"; reg = <0x41>; shunt-milliohms = <5>; + // 2S NP-F550 pack + power-supply; + power-supply-reference-voltage-mv = <8400>; + power-supply-min-voltage-mv = <6400>; }; sc2356 { @@ -106,6 +126,42 @@ }; }; + exp0_rf_internal_external { + compatible = "gpio-hog"; + pin = <&io_expander0 0 GPIO_FLAG_NONE>; + mode = ; + }; + + exp0_external_5v_bus_enable { + compatible = "gpio-hog"; + pin = <&io_expander0 2 GPIO_FLAG_NONE>; + mode = ; + }; + + exp0_camera_reset { + compatible = "gpio-hog"; + pin = <&io_expander0 6 GPIO_FLAG_NONE>; + mode = ; + }; + + exp1_c6_wlan_enable { + compatible = "gpio-hog"; + pin = <&io_expander1 0 GPIO_FLAG_NONE>; + mode = ; + }; + + exp1_usb_a_5v_enable { + compatible = "gpio-hog"; + pin = <&io_expander1 3 GPIO_FLAG_NONE>; + mode = ; + }; + + exp1_ip2326_chg_stat_led { + compatible = "gpio-hog"; + pin = <&io_expander1 6 GPIO_FLAG_NONE>; + mode = ; + }; + port_a: grove0 { compatible = "espressif,esp32-grove"; defaultMode = ; @@ -145,17 +201,14 @@ usbhosthid0 { compatible = "espressif,esp32-usbhost-hid"; - //status = "disabled"; }; usbhostmidi0 { compatible = "espressif,esp32-usbhost-midi"; - //status = "disabled"; }; usbhostmsc0 { compatible = "espressif,esp32-usbhost-msc"; - //status = "disabled"; }; }; }; diff --git a/Devices/waveshare-esp32-s3-geek/CMakeLists.txt b/Devices/waveshare-esp32-s3-geek/CMakeLists.txt index 0ab2023a9..38c007833 100644 --- a/Devices/waveshare-esp32-s3-geek/CMakeLists.txt +++ b/Devices/waveshare-esp32-s3-geek/CMakeLists.txt @@ -1,7 +1,7 @@ -file(GLOB_RECURSE SOURCE_FILES Source/*.c*) - -idf_component_register( - SRCS ${SOURCE_FILES} - INCLUDE_DIRS "Source" - REQUIRES Tactility ButtonControl ST7789 PwmBacklight driver -) \ No newline at end of file +file(GLOB_RECURSE SOURCE_FILES source/*.c*) + +idf_component_register( + SRCS ${SOURCE_FILES} + INCLUDE_DIRS "source" + REQUIRES TactilityKernel driver +) diff --git a/Devices/waveshare-esp32-s3-geek/Source/Configuration.cpp b/Devices/waveshare-esp32-s3-geek/Source/Configuration.cpp deleted file mode 100644 index 5f28b1f3c..000000000 --- a/Devices/waveshare-esp32-s3-geek/Source/Configuration.cpp +++ /dev/null @@ -1,23 +0,0 @@ -#include "devices/Display.h" - -#include -#include -#include - -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 -}; \ No newline at end of file diff --git a/Devices/waveshare-esp32-s3-geek/Source/devices/Display.cpp b/Devices/waveshare-esp32-s3-geek/Source/devices/Display.cpp deleted file mode 100644 index 86329c4e1..000000000 --- a/Devices/waveshare-esp32-s3-geek/Source/devices/Display.cpp +++ /dev/null @@ -1,32 +0,0 @@ -#include "Display.h" - -#include -#include - -std::shared_ptr createDisplay() { - St7789Display::Configuration panel_configuration = { - .horizontalResolution = LCD_HORIZONTAL_RESOLUTION, - .verticalResolution = LCD_VERTICAL_RESOLUTION, - .gapX = 52, - .gapY = 40, - .swapXY = false, - .mirrorX = 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 { - .spiHostDevice = LCD_SPI_HOST, - .csPin = LCD_PIN_CS, - .dcPin = LCD_PIN_DC, - .pixelClockFrequency = 62'500'000, - .transactionQueueDepth = 10 - }); - - return std::make_shared(panel_configuration, spi_configuration); -} diff --git a/Devices/waveshare-esp32-s3-geek/Source/devices/Display.h b/Devices/waveshare-esp32-s3-geek/Source/devices/Display.h deleted file mode 100644 index 628270acf..000000000 --- a/Devices/waveshare-esp32-s3-geek/Source/devices/Display.h +++ /dev/null @@ -1,18 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -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 createDisplay(); \ No newline at end of file diff --git a/Devices/waveshare-esp32-s3-geek/device.properties b/Devices/waveshare-esp32-s3-geek/device.properties index 19b06a31b..e9af77607 100644 --- a/Devices/waveshare-esp32-s3-geek/device.properties +++ b/Devices/waveshare-esp32-s3-geek/device.properties @@ -14,6 +14,8 @@ hardware.tinyUsb=true hardware.esptoolFlashFreq=120M hardware.bluetooth=true +dependencies.useDeprecatedHal=false + storage.userDataLocation=SD display.size=1.14" diff --git a/Devices/waveshare-esp32-s3-geek/devicetree.yaml b/Devices/waveshare-esp32-s3-geek/devicetree.yaml index e85141149..094aef947 100644 --- a/Devices/waveshare-esp32-s3-geek/devicetree.yaml +++ b/Devices/waveshare-esp32-s3-geek/devicetree.yaml @@ -1,3 +1,5 @@ dependencies: - Platforms/platform-esp32 + - Drivers/st7789-module + - Drivers/button-control-module dts: waveshare,esp32-s3-geek.dts diff --git a/Devices/waveshare-esp32-s3-geek/Source/module.cpp b/Devices/waveshare-esp32-s3-geek/source/module.cpp similarity index 100% rename from Devices/waveshare-esp32-s3-geek/Source/module.cpp rename to Devices/waveshare-esp32-s3-geek/source/module.cpp diff --git a/Devices/waveshare-esp32-s3-geek/waveshare,esp32-s3-geek.dts b/Devices/waveshare-esp32-s3-geek/waveshare,esp32-s3-geek.dts index 9b4b9e5d9..fb532bea5 100644 --- a/Devices/waveshare-esp32-s3-geek/waveshare,esp32-s3-geek.dts +++ b/Devices/waveshare-esp32-s3-geek/waveshare,esp32-s3-geek.dts @@ -8,7 +8,11 @@ #include #include #include -#include +#include +#include + +#include +#include / { compatible = "root"; @@ -37,18 +41,50 @@ 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 { compatible = "espressif,esp32-spi"; host = ; cs-gpios = <&gpio0 10 GPIO_FLAG_NONE>; pin-mosi = <&gpio0 11 GPIO_FLAG_NONE>; pin-sclk = <&gpio0 12 GPIO_FLAG_NONE>; - - display { - compatible = "display-placeholder"; + + display@0 { + 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 { compatible = "espressif,esp32-sdmmc"; pin-clk = <&gpio0 36 GPIO_FLAG_NONE>; diff --git a/Devices/waveshare-s3-lcd-13/source/module.cpp b/Devices/waveshare-s3-lcd-13/source/module.cpp index ea67b6649..421dc26e7 100644 --- a/Devices/waveshare-s3-lcd-13/source/module.cpp +++ b/Devices/waveshare-s3-lcd-13/source/module.cpp @@ -1,19 +1,12 @@ +#include #include extern "C" { -static error_t start() { - return ERROR_NONE; -} - -static error_t stop() { - return ERROR_NONE; -} - -struct Module waveshare_s3_lcd_13_module = { +Module waveshare_s3_lcd_13_module = { .name = "waveshare-s3-lcd-13", - .start = start, - .stop = stop, + .start = [] -> error_t { return ERROR_NONE; }, + .stop = [] -> error_t { return ERROR_NONE; }, .symbols = nullptr, .internal = nullptr }; diff --git a/Devices/waveshare-s3-touch-lcd-128/CMakeLists.txt b/Devices/waveshare-s3-touch-lcd-128/CMakeLists.txt index 875eee8e1..09a2fdee3 100644 --- a/Devices/waveshare-s3-touch-lcd-128/CMakeLists.txt +++ b/Devices/waveshare-s3-touch-lcd-128/CMakeLists.txt @@ -1,7 +1,7 @@ -file(GLOB_RECURSE SOURCE_FILES Source/*.c*) +file(GLOB_RECURSE SOURCE_FILES source/*.c*) idf_component_register( SRCS ${SOURCE_FILES} - INCLUDE_DIRS "Source" - REQUIRES Tactility esp_lvgl_port GC9A01 CST816S PwmBacklight driver + INCLUDE_DIRS "source" + REQUIRES TactilityKernel ) diff --git a/Devices/waveshare-s3-touch-lcd-128/Source/Configuration.cpp b/Devices/waveshare-s3-touch-lcd-128/Source/Configuration.cpp deleted file mode 100644 index 626c12a9b..000000000 --- a/Devices/waveshare-s3-touch-lcd-128/Source/Configuration.cpp +++ /dev/null @@ -1,21 +0,0 @@ -#include "devices/Display.h" - -#include -#include - -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 -}; diff --git a/Devices/waveshare-s3-touch-lcd-128/Source/devices/Display.cpp b/Devices/waveshare-s3-touch-lcd-128/Source/devices/Display.cpp deleted file mode 100644 index f152d94f6..000000000 --- a/Devices/waveshare-s3-touch-lcd-128/Source/devices/Display.cpp +++ /dev/null @@ -1,45 +0,0 @@ -#include "Display.h" - -#include -#include -#include - -std::shared_ptr createTouch() { - auto configuration = std::make_unique( - I2C_NUM_0, - 240, - 240, - false, - true, - true, - GPIO_NUM_13, - GPIO_NUM_5 - ); - - return std::make_shared(std::move(configuration)); -} - -std::shared_ptr createDisplay() { - auto touch = createTouch(); - - auto configuration = std::make_unique( - 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(std::move(configuration)); - return std::reinterpret_pointer_cast(display); -} diff --git a/Devices/waveshare-s3-touch-lcd-128/Source/devices/Display.h b/Devices/waveshare-s3-touch-lcd-128/Source/devices/Display.h deleted file mode 100644 index b3ce9fb57..000000000 --- a/Devices/waveshare-s3-touch-lcd-128/Source/devices/Display.h +++ /dev/null @@ -1,5 +0,0 @@ -#pragma once - -#include "Tactility/hal/display/DisplayDevice.h" - -std::shared_ptr createDisplay(); \ No newline at end of file diff --git a/Devices/waveshare-s3-touch-lcd-128/Source/module.cpp b/Devices/waveshare-s3-touch-lcd-128/Source/module.cpp deleted file mode 100644 index 5423ad639..000000000 --- a/Devices/waveshare-s3-touch-lcd-128/Source/module.cpp +++ /dev/null @@ -1,23 +0,0 @@ -#include - -extern "C" { - -static error_t start() { - // Empty for now - return ERROR_NONE; -} - -static error_t stop() { - // Empty for now - return ERROR_NONE; -} - -struct Module waveshare_s3_touch_lcd_128_module = { - .name = "waveshare-s3-touch-lcd-128", - .start = start, - .stop = stop, - .symbols = nullptr, - .internal = nullptr -}; - -} diff --git a/Devices/waveshare-s3-touch-lcd-128/device.properties b/Devices/waveshare-s3-touch-lcd-128/device.properties index 071fa9f95..d9fe86382 100644 --- a/Devices/waveshare-s3-touch-lcd-128/device.properties +++ b/Devices/waveshare-s3-touch-lcd-128/device.properties @@ -14,7 +14,9 @@ hardware.tinyUsb=true hardware.esptoolFlashFreq=120M hardware.bluetooth=true -storage.userDataLocation=SD +dependencies.useDeprecatedHal=false + +storage.userDataLocation=Internal display.size=1.28" display.shape=circle diff --git a/Devices/waveshare-s3-touch-lcd-128/devicetree.yaml b/Devices/waveshare-s3-touch-lcd-128/devicetree.yaml index 117dac1af..bf6b838d7 100644 --- a/Devices/waveshare-s3-touch-lcd-128/devicetree.yaml +++ b/Devices/waveshare-s3-touch-lcd-128/devicetree.yaml @@ -1,4 +1,6 @@ dependencies: - Platforms/platform-esp32 - Drivers/qmi8658-module +- Drivers/cst816t-module +- Drivers/gc9a01-module dts: waveshare,s3-touch-lcd-128.dts diff --git a/Devices/waveshare-s3-touch-lcd-128/source/module.cpp b/Devices/waveshare-s3-touch-lcd-128/source/module.cpp new file mode 100644 index 000000000..4d594ac77 --- /dev/null +++ b/Devices/waveshare-s3-touch-lcd-128/source/module.cpp @@ -0,0 +1,14 @@ +#include +#include + +extern "C" { + +Module waveshare_s3_touch_lcd_128_module = { + .name = "waveshare-s3-touch-lcd-128", + .start = [] -> error_t { return ERROR_NONE; }, + .stop = [] -> error_t { return ERROR_NONE; }, + .symbols = nullptr, + .internal = nullptr +}; + +} diff --git a/Devices/waveshare-s3-touch-lcd-128/waveshare,s3-touch-lcd-128.dts b/Devices/waveshare-s3-touch-lcd-128/waveshare,s3-touch-lcd-128.dts index ea6a295a8..c91134fdc 100644 --- a/Devices/waveshare-s3-touch-lcd-128/waveshare,s3-touch-lcd-128.dts +++ b/Devices/waveshare-s3-touch-lcd-128/waveshare,s3-touch-lcd-128.dts @@ -8,7 +8,10 @@ #include #include #include -#include +#include +#include +#include +#include // Reference: https://www.waveshare.com/wiki/ESP32-S3-Touch-LCD-1.28 / { @@ -41,6 +44,33 @@ compatible = "qst,qmi8658"; reg = <0x6B>; }; + + touch0 { + compatible = "hynitron,cst816t"; + reg = <0x15>; + x-max = <240>; + y-max = <240>; + mirror-x; + mirror-y; + pin-reset = <&gpio0 13 GPIO_FLAG_NONE>; + pin-interrupt = <&gpio0 5 GPIO_FLAG_NONE>; + }; + }; + + display_backlight_pwm { + compatible = "espressif,esp32-pwm-ledc"; + pin = <&gpio0 2 GPIO_FLAG_NONE>; + period-ns = <3906250>; + 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 { @@ -50,23 +80,17 @@ pin-mosi = <&gpio0 11 GPIO_FLAG_NONE>; pin-miso = <&gpio0 12 GPIO_FLAG_NONE>; pin-sclk = <&gpio0 10 GPIO_FLAG_NONE>; - - display { - compatible = "display-placeholder"; - }; - }; - spi1 { - compatible = "espressif,esp32-spi"; - host = ; - cs-gpios = <&gpio0 18 GPIO_FLAG_NONE>; - pin-mosi = <&gpio0 16 GPIO_FLAG_NONE>; - pin-miso = <&gpio0 15 GPIO_FLAG_NONE>; - pin-sclk = <&gpio0 17 GPIO_FLAG_NONE>; - - sdcard@0 { - compatible = "espressif,esp32-sdspi"; - frequency-khz = <20000>; + display@0 { + compatible = "galaxycore,gc9a01"; + horizontal-resolution = <240>; + vertical-resolution = <240>; + mirror-y; + invert-color; + bgr-order; + pin-dc = <&gpio0 8 GPIO_FLAG_NONE>; + pin-reset = <&gpio0 14 GPIO_FLAG_NONE>; + backlight = <&display_backlight>; }; }; }; diff --git a/Devices/waveshare-s3-touch-lcd-147/CMakeLists.txt b/Devices/waveshare-s3-touch-lcd-147/CMakeLists.txt index 68ceecc20..09a2fdee3 100644 --- a/Devices/waveshare-s3-touch-lcd-147/CMakeLists.txt +++ b/Devices/waveshare-s3-touch-lcd-147/CMakeLists.txt @@ -1,7 +1,7 @@ -file(GLOB_RECURSE SOURCE_FILES Source/*.c*) +file(GLOB_RECURSE SOURCE_FILES source/*.c*) idf_component_register( SRCS ${SOURCE_FILES} - INCLUDE_DIRS "Source" - REQUIRES Tactility esp_lvgl_port EspLcdCompat AXS5106 JD9853 + INCLUDE_DIRS "source" + REQUIRES TactilityKernel ) diff --git a/Devices/waveshare-s3-touch-lcd-147/Source/Configuration.cpp b/Devices/waveshare-s3-touch-lcd-147/Source/Configuration.cpp deleted file mode 100644 index dbd7799cf..000000000 --- a/Devices/waveshare-s3-touch-lcd-147/Source/Configuration.cpp +++ /dev/null @@ -1,18 +0,0 @@ -#include "devices/Display.h" - -#include - -bool initBoot(); - -using namespace tt::hal; - -static std::vector> createDevices() { - return { - createDisplay(), - }; -} - -extern const Configuration hardwareConfiguration = { - .initBoot = initBoot, - .createDevices = createDevices -}; diff --git a/Devices/waveshare-s3-touch-lcd-147/Source/Init.cpp b/Devices/waveshare-s3-touch-lcd-147/Source/Init.cpp deleted file mode 100644 index 83f92a60b..000000000 --- a/Devices/waveshare-s3-touch-lcd-147/Source/Init.cpp +++ /dev/null @@ -1,45 +0,0 @@ -#include -#include -#include -#include - -constexpr auto* TAG = "Waveshare"; - -constexpr auto LCD_BL_LEDC_TIMER = LEDC_TIMER_0; -constexpr auto LCD_BL_LEDC_MODE = LEDC_LOW_SPEED_MODE; -constexpr auto LCD_BL_LEDC_CHANNEL = LEDC_CHANNEL_0; -constexpr auto LCD_BL_LEDC_DUTY_RES = LEDC_TIMER_10_BIT; -constexpr auto LCD_BL_LEDC_DUTY = 1024; -constexpr auto LCD_BL_LEDC_FREQUENCY = 10000; - -void setBacklightDuty(uint8_t level) { - uint32_t duty = (level * (LCD_BL_LEDC_DUTY - 1)) / 255; - ESP_ERROR_CHECK(ledc_set_duty(LCD_BL_LEDC_MODE, LCD_BL_LEDC_CHANNEL, duty)); - ESP_ERROR_CHECK(ledc_update_duty(LCD_BL_LEDC_MODE, LCD_BL_LEDC_CHANNEL)); -} - -void initBacklight() { - ledc_timer_config_t timer_config = { - .speed_mode = LCD_BL_LEDC_MODE, - .duty_resolution = LCD_BL_LEDC_DUTY_RES, - .timer_num = LCD_BL_LEDC_TIMER, - .freq_hz = LCD_BL_LEDC_FREQUENCY, - .clk_cfg = LEDC_AUTO_CLK}; - ESP_ERROR_CHECK(ledc_timer_config(&timer_config)); - - ledc_channel_config_t channel_config = { - .gpio_num = GPIO_NUM_46, - .speed_mode = LCD_BL_LEDC_MODE, - .channel = LCD_BL_LEDC_CHANNEL, - .intr_type = LEDC_INTR_DISABLE, - .timer_sel = LCD_BL_LEDC_TIMER, - .duty = 0, // Set duty to 0% - .hpoint = 0}; - ESP_ERROR_CHECK(ledc_channel_config(&channel_config)); -} - -bool initBoot() { - LOG_I(TAG, LOG_MESSAGE_POWER_ON_START); - initBacklight(); - return true; -} diff --git a/Devices/waveshare-s3-touch-lcd-147/Source/devices/Axs5106Touch.cpp b/Devices/waveshare-s3-touch-lcd-147/Source/devices/Axs5106Touch.cpp deleted file mode 100644 index a56aa279b..000000000 --- a/Devices/waveshare-s3-touch-lcd-147/Source/devices/Axs5106Touch.cpp +++ /dev/null @@ -1,35 +0,0 @@ -#include "Axs5106Touch.h" - -#include -#include - -bool Axs5106Touch::createIoHandle(esp_lcd_panel_io_handle_t& outHandle) { - esp_lcd_panel_io_i2c_config_t io_config = ESP_LCD_TOUCH_IO_I2C_AXS5106_CONFIG(); - return esp_lcd_new_panel_io_i2c(configuration->port, &io_config, &outHandle) == ESP_OK; -} - -bool Axs5106Touch::createTouchHandle(esp_lcd_panel_io_handle_t ioHandle, const esp_lcd_touch_config_t& configuration, esp_lcd_touch_handle_t& panelHandle) { - return esp_lcd_touch_new_i2c_axs5106(ioHandle, &configuration, &panelHandle) == ESP_OK; -} - -esp_lcd_touch_config_t Axs5106Touch::createEspLcdTouchConfig() { - return { - .x_max = configuration->xMax, - .y_max = configuration->yMax, - .rst_gpio_num = configuration->pinReset, - .int_gpio_num = configuration->pinInterrupt, - .levels = { - .reset = configuration->pinResetLevel, - .interrupt = configuration->pinInterruptLevel, - }, - .flags = { - .swap_xy = configuration->swapXy, - .mirror_x = configuration->mirrorX, - .mirror_y = configuration->mirrorY, - }, - .process_coordinates = nullptr, - .interrupt_callback = nullptr, - .user_data = nullptr, - .driver_data = nullptr - }; -} diff --git a/Devices/waveshare-s3-touch-lcd-147/Source/devices/Axs5106Touch.h b/Devices/waveshare-s3-touch-lcd-147/Source/devices/Axs5106Touch.h deleted file mode 100644 index 7848d1864..000000000 --- a/Devices/waveshare-s3-touch-lcd-147/Source/devices/Axs5106Touch.h +++ /dev/null @@ -1,70 +0,0 @@ -#pragma once - -#include -#include -#include - -#include - -class Axs5106Touch final : public EspLcdTouch { - -public: - - class Configuration { - public: - - Configuration( - i2c_port_t port, - uint16_t xMax, - uint16_t yMax, - bool swapXy = false, - bool mirrorX = false, - bool mirrorY = false, - gpio_num_t pinReset = GPIO_NUM_NC, - gpio_num_t pinInterrupt = GPIO_NUM_NC, - unsigned int pinResetLevel = 0, - unsigned int pinInterruptLevel = 0 - ) : port(port), - xMax(xMax), - yMax(yMax), - swapXy(swapXy), - mirrorX(mirrorX), - mirrorY(mirrorY), - pinReset(pinReset), - pinInterrupt(pinInterrupt), - pinResetLevel(pinResetLevel), - pinInterruptLevel(pinInterruptLevel) - {} - - i2c_port_t port; - uint16_t xMax; - uint16_t yMax; - bool swapXy; - bool mirrorX; - bool mirrorY; - gpio_num_t pinReset; - gpio_num_t pinInterrupt; - unsigned int pinResetLevel; - unsigned int pinInterruptLevel; - }; - -private: - - std::unique_ptr configuration; - - bool createIoHandle(esp_lcd_panel_io_handle_t& outHandle) override; - - bool createTouchHandle(esp_lcd_panel_io_handle_t ioHandle, const esp_lcd_touch_config_t& configuration, esp_lcd_touch_handle_t& panelHandle) override; - - esp_lcd_touch_config_t createEspLcdTouchConfig() override; - -public: - - explicit Axs5106Touch(std::unique_ptr inConfiguration) : configuration(std::move(inConfiguration)) { - assert(configuration != nullptr); - } - - std::string getName() const override { return "AXS5106"; } - - std::string getDescription() const override { return "AXS5106 I2C touch driver"; } -}; diff --git a/Devices/waveshare-s3-touch-lcd-147/Source/devices/Display.cpp b/Devices/waveshare-s3-touch-lcd-147/Source/devices/Display.cpp deleted file mode 100644 index 5834a74be..000000000 --- a/Devices/waveshare-s3-touch-lcd-147/Source/devices/Display.cpp +++ /dev/null @@ -1,50 +0,0 @@ -#include "Display.h" -#include "Jd9853Display.h" -#include "Axs5106Touch.h" - -constexpr auto LCD_SPI_HOST = SPI2_HOST; -constexpr auto LCD_PIN_CS = GPIO_NUM_21; -constexpr auto LCD_PIN_DC = GPIO_NUM_45; -constexpr auto LCD_PIN_RESET = GPIO_NUM_40; -constexpr auto LCD_HORIZONTAL_RESOLUTION = 172; -constexpr auto LCD_VERTICAL_RESOLUTION = 320; - -void setBacklightDuty(uint8_t level); - -static std::shared_ptr createTouch() { - auto configuration = std::make_unique( - I2C_NUM_0, - 172, - 320, - false, - false, - false, - GPIO_NUM_47, - GPIO_NUM_48 - ); - - return std::make_shared(std::move(configuration)); -} - -std::shared_ptr createDisplay() { - auto touch = createTouch(); - - auto configuration = std::make_unique( - LCD_SPI_HOST, - LCD_PIN_CS, - LCD_PIN_DC, - LCD_PIN_RESET, - 172, - 320, - touch, - false, - false, - false, - true - ); - - configuration->backlightDutyFunction = setBacklightDuty; - - auto display = std::make_shared(std::move(configuration)); - return std::reinterpret_pointer_cast(display); -} diff --git a/Devices/waveshare-s3-touch-lcd-147/Source/devices/Display.h b/Devices/waveshare-s3-touch-lcd-147/Source/devices/Display.h deleted file mode 100644 index 7a9b967d1..000000000 --- a/Devices/waveshare-s3-touch-lcd-147/Source/devices/Display.h +++ /dev/null @@ -1,5 +0,0 @@ -#pragma once - -#include - -std::shared_ptr createDisplay(); diff --git a/Devices/waveshare-s3-touch-lcd-147/Source/devices/Jd9853Display.cpp b/Devices/waveshare-s3-touch-lcd-147/Source/devices/Jd9853Display.cpp deleted file mode 100644 index 78b550d54..000000000 --- a/Devices/waveshare-s3-touch-lcd-147/Source/devices/Jd9853Display.cpp +++ /dev/null @@ -1,161 +0,0 @@ -#include "Jd9853Display.h" - -#include -#include -#include -#include - -constexpr auto* TAG = "JD9853"; - -bool Jd9853Display::createIoHandle(esp_lcd_panel_io_handle_t& outHandle) { - const esp_lcd_panel_io_spi_config_t panel_io_config = { - .cs_gpio_num = configuration->csPin, - .dc_gpio_num = configuration->dcPin, - .spi_mode = 0, - .pclk_hz = configuration->pixelClockFrequency, - .trans_queue_depth = configuration->transactionQueueDepth, - .on_color_trans_done = nullptr, - .user_ctx = nullptr, - .lcd_cmd_bits = 8, - .lcd_param_bits = 8, - .cs_ena_pretrans = 0, - .cs_ena_posttrans = 0, - .flags = { - .dc_high_on_cmd = 0, - .dc_low_on_data = 0, - .dc_low_on_param = 0, - .octal_mode = 0, - .quad_mode = 0, - .sio_mode = 0, - .lsb_first = 0, - .cs_high_active = 0 - } - }; - - return esp_lcd_new_panel_io_spi(configuration->spiHostDevice, &panel_io_config, &outHandle) == ESP_OK; -} - -bool Jd9853Display::createPanelHandle(esp_lcd_panel_io_handle_t ioHandle, esp_lcd_panel_handle_t& panelHandle) { - jd9853_vendor_config_t vendor_config = { - .init_cmds = nullptr, - .init_cmds_size = 0 - }; - - const esp_lcd_panel_dev_config_t panel_config = { - .reset_gpio_num = configuration->resetPin, - .rgb_ele_order = configuration->rgbElementOrder, - .data_endian = LCD_RGB_DATA_ENDIAN_LITTLE, - .bits_per_pixel = 16, - .flags = { - .reset_active_high = false - }, - .vendor_config = &vendor_config - }; - - if (esp_lcd_new_panel_jd9853(ioHandle, &panel_config, &panelHandle) != ESP_OK) { - LOG_E(TAG, "Failed to create panel"); - return false; - } - - if (esp_lcd_panel_reset(panelHandle) != ESP_OK) { - LOG_E(TAG, "Failed to reset panel"); - return false; - } - - if (esp_lcd_panel_init(panelHandle) != ESP_OK) { - LOG_E(TAG, "Failed to init panel"); - return false; - } - - if (esp_lcd_panel_swap_xy(panelHandle, configuration->swapXY) != ESP_OK) { - LOG_E(TAG, "Failed to swap XY"); - return false; - } - - if (esp_lcd_panel_mirror(panelHandle, configuration->mirrorX, configuration->mirrorY) != ESP_OK) { - LOG_E(TAG, "Failed to set panel to mirror"); - return false; - } - - if (esp_lcd_panel_invert_color(panelHandle, configuration->invertColor) != ESP_OK) { - LOG_E(TAG, "Failed to set panel to invert"); - return false; - } - - if (esp_lcd_panel_disp_on_off(panelHandle, true) != ESP_OK) { - LOG_E(TAG, "Failed to turn display on"); - return false; - } - - if (esp_lcd_panel_set_gap(panelHandle, 34, 0) != ESP_OK) { - LOG_E(TAG, "Failed to set panel gap"); - return false; - } - - return true; -} - -lvgl_port_display_cfg_t Jd9853Display::getLvglPortDisplayConfig(esp_lcd_panel_io_handle_t ioHandle, esp_lcd_panel_handle_t panelHandle) { - return { - .io_handle = ioHandle, - .panel_handle = panelHandle, - .control_handle = nullptr, - .buffer_size = configuration->bufferSize, - .double_buffer = false, - .trans_size = 0, - .hres = configuration->horizontalResolution, - .vres = configuration->verticalResolution, - .monochrome = false, - .rotation = { - .swap_xy = configuration->swapXY, - .mirror_x = configuration->mirrorX, - .mirror_y = configuration->mirrorY, - }, - .color_format = LV_COLOR_FORMAT_RGB565, - .flags = { - .buff_dma = true, - .buff_spiram = false, - .sw_rotate = false, - .swap_bytes = true, - .full_refresh = false, - .direct_mode = false - } - }; -} - -/** - * Note: - * The datasheet implies this should work, but it doesn't: - * https://www.digikey.com/htmldatasheets/production/1640716/0/0/1/ILI9341-Datasheet.pdf - * - * This repo claims it only has 1 curve: - * https://github.com/brucemack/hello-ili9341 - * - * I'm leaving it in as I'm not sure if it's just my hardware that's problematic. - */ -void Jd9853Display::setGammaCurve(uint8_t index) { - uint8_t gamma_curve; - switch (index) { - case 0: - gamma_curve = 0x01; - break; - case 1: - gamma_curve = 0x04; - break; - case 2: - gamma_curve = 0x02; - break; - case 3: - gamma_curve = 0x08; - break; - default: - return; - } - const uint8_t param[] = { - gamma_curve - }; - - if (esp_lcd_panel_io_tx_param(getIoHandle() , LCD_CMD_GAMSET, param, 1) != ESP_OK) { - LOG_E(TAG, "Failed to set gamma"); - } -} diff --git a/Devices/waveshare-s3-touch-lcd-147/Source/devices/Jd9853Display.h b/Devices/waveshare-s3-touch-lcd-147/Source/devices/Jd9853Display.h deleted file mode 100644 index b1430e034..000000000 --- a/Devices/waveshare-s3-touch-lcd-147/Source/devices/Jd9853Display.h +++ /dev/null @@ -1,106 +0,0 @@ -#pragma once - -#include - -#include - -#include -#include -#include -#include -#include -#include - -class Jd9853Display final : public EspLcdDisplay { - -public: - - class Configuration { - - public: - - Configuration( - spi_host_device_t spiHostDevice, - gpio_num_t csPin, - gpio_num_t dcPin, - gpio_num_t resetPin, - unsigned int horizontalResolution, - unsigned int verticalResolution, - std::shared_ptr touch, - bool swapXY = false, - bool mirrorX = false, - bool mirrorY = false, - bool invertColor = false, - uint32_t bufferSize = 0, // Size in pixel count. 0 means default, which is 1/10 of the screen size, - lcd_rgb_element_order_t rgbElementOrder = LCD_RGB_ELEMENT_ORDER_RGB - ) : spiHostDevice(spiHostDevice), - csPin(csPin), - dcPin(dcPin), - resetPin(resetPin), - horizontalResolution(horizontalResolution), - verticalResolution(verticalResolution), - swapXY(swapXY), - mirrorX(mirrorX), - mirrorY(mirrorY), - invertColor(invertColor), - bufferSize(bufferSize), - rgbElementOrder(rgbElementOrder), - touch(std::move(touch) - ) { - if (this->bufferSize == 0) { - this->bufferSize = horizontalResolution * verticalResolution / 10; - } - } - - spi_host_device_t spiHostDevice; - gpio_num_t csPin; - gpio_num_t dcPin; - gpio_num_t resetPin; - unsigned int pixelClockFrequency = 40'000'000; // Hertz - size_t transactionQueueDepth = 10; - unsigned int horizontalResolution; - unsigned int verticalResolution; - bool swapXY; - bool mirrorX; - bool mirrorY; - bool invertColor; - uint32_t bufferSize; // Size in pixel count. 0 means default, which is 1/10 of the screen size - lcd_rgb_element_order_t rgbElementOrder; - std::shared_ptr touch; - std::function backlightDutyFunction = nullptr; - }; - -private: - - std::unique_ptr configuration; - - bool createIoHandle(esp_lcd_panel_io_handle_t& outHandle) override; - - bool createPanelHandle(esp_lcd_panel_io_handle_t ioHandle, esp_lcd_panel_handle_t& panelHandle) override; - - lvgl_port_display_cfg_t getLvglPortDisplayConfig(esp_lcd_panel_io_handle_t ioHandle, esp_lcd_panel_handle_t panelHandle) override; - -public: - - explicit Jd9853Display(std::unique_ptr inConfiguration) : - configuration(std::move(inConfiguration) - ) {} - - std::string getName() const override { return "JD9853"; } - - std::string getDescription() const override { return "JD9853 display"; } - - std::shared_ptr getTouchDevice() override { return configuration->touch; } - - void setBacklightDuty(uint8_t backlightDuty) override { - if (configuration->backlightDutyFunction != nullptr) { - configuration->backlightDutyFunction(backlightDuty); - } - } - - bool supportsBacklightDuty() const override { return configuration->backlightDutyFunction != nullptr; } - - void setGammaCurve(uint8_t index) override; - - uint8_t getGammaCurveCount() const override { return 4; }; -}; diff --git a/Devices/waveshare-s3-touch-lcd-147/Source/module.cpp b/Devices/waveshare-s3-touch-lcd-147/Source/module.cpp deleted file mode 100644 index 202a510ae..000000000 --- a/Devices/waveshare-s3-touch-lcd-147/Source/module.cpp +++ /dev/null @@ -1,23 +0,0 @@ -#include - -extern "C" { - -static error_t start() { - // Empty for now - return ERROR_NONE; -} - -static error_t stop() { - // Empty for now - return ERROR_NONE; -} - -struct Module waveshare_s3_touch_lcd_147_module = { - .name = "waveshare-s3-touch-lcd-147", - .start = start, - .stop = stop, - .symbols = nullptr, - .internal = nullptr -}; - -} diff --git a/Devices/waveshare-s3-touch-lcd-147/device.properties b/Devices/waveshare-s3-touch-lcd-147/device.properties index 092fa342c..f115c77f1 100644 --- a/Devices/waveshare-s3-touch-lcd-147/device.properties +++ b/Devices/waveshare-s3-touch-lcd-147/device.properties @@ -3,7 +3,6 @@ general.name=S3 Touch LCD 1.47" general.incubating=true apps.launcherAppId=Launcher -apps.autoStartAppId=ApWebServer hardware.target=ESP32S3 hardware.flashSize=16MB @@ -14,13 +13,13 @@ hardware.tinyUsb=true hardware.esptoolFlashFreq=120M hardware.bluetooth=true +dependencies.useDeprecatedHal=false + storage.userDataLocation=SD display.size=1.47" display.shape=rectangle display.dpi=247 -cdn.warningMessage=Touch doesn't work yet - lvgl.colorDepth=16 lvgl.uiDensity=compact diff --git a/Devices/waveshare-s3-touch-lcd-147/devicetree.yaml b/Devices/waveshare-s3-touch-lcd-147/devicetree.yaml index 6712e245d..eab1c9d48 100644 --- a/Devices/waveshare-s3-touch-lcd-147/devicetree.yaml +++ b/Devices/waveshare-s3-touch-lcd-147/devicetree.yaml @@ -1,3 +1,6 @@ dependencies: - Platforms/platform-esp32 + - Drivers/axs5106-module + - Drivers/button-control-module + - Drivers/jd9853-module dts: waveshare,s3-touch-lcd-147.dts diff --git a/Devices/waveshare-s3-touch-lcd-147/source/module.cpp b/Devices/waveshare-s3-touch-lcd-147/source/module.cpp new file mode 100644 index 000000000..7af30a280 --- /dev/null +++ b/Devices/waveshare-s3-touch-lcd-147/source/module.cpp @@ -0,0 +1,14 @@ +#include +#include + +extern "C" { + +Module waveshare_s3_touch_lcd_147_module = { + .name = "waveshare-s3-touch-lcd-147", + .start = [] -> error_t { return ERROR_NONE; }, + .stop = [] -> error_t { return ERROR_NONE; }, + .symbols = nullptr, + .internal = nullptr +}; + +} diff --git a/Devices/waveshare-s3-touch-lcd-147/waveshare,s3-touch-lcd-147.dts b/Devices/waveshare-s3-touch-lcd-147/waveshare,s3-touch-lcd-147.dts index 0f4b7613d..891dee234 100644 --- a/Devices/waveshare-s3-touch-lcd-147/waveshare,s3-touch-lcd-147.dts +++ b/Devices/waveshare-s3-touch-lcd-147/waveshare,s3-touch-lcd-147.dts @@ -7,7 +7,11 @@ #include #include #include -#include +#include +#include +#include +#include +#include // Reference: https://www.waveshare.com/wiki/ESP32-S3-Touch-LCD-1.47 / { @@ -35,6 +39,32 @@ clock-frequency = <400000>; pin-sda = <&gpio0 42 GPIO_FLAG_NONE>; pin-scl = <&gpio0 41 GPIO_FLAG_NONE>; + + touch0 { + compatible = "axs,axs5106"; + reg = <0x63>; + x-max = <172>; + y-max = <320>; + mirror-x; + pin-reset = <&gpio0 47 GPIO_FLAG_NONE>; + pin-interrupt = <&gpio0 48 GPIO_FLAG_NONE>; + }; + }; + + display_backlight_pwm { + compatible = "espressif,esp32-pwm-ledc"; + pin = <&gpio0 46 GPIO_FLAG_NONE>; + period-ns = <100000>; + 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 { @@ -44,8 +74,15 @@ pin-mosi = <&gpio0 39 GPIO_FLAG_NONE>; pin-sclk = <&gpio0 38 GPIO_FLAG_NONE>; - display { - compatible = "display-placeholder"; + display@0 { + compatible = "jadard,jd9853"; + horizontal-resolution = <172>; + vertical-resolution = <320>; + gap-x = <34>; + invert-color; + pin-dc = <&gpio0 45 GPIO_FLAG_NONE>; + pin-reset = <&gpio0 40 GPIO_FLAG_NONE>; + backlight = <&display_backlight>; }; }; @@ -56,10 +93,15 @@ pin-mosi = <&gpio0 15 GPIO_FLAG_NONE>; pin-miso = <&gpio0 17 GPIO_FLAG_NONE>; pin-sclk = <&gpio0 16 GPIO_FLAG_NONE>; - + sdcard@0 { compatible = "espressif,esp32-sdspi"; frequency-khz = <20000>; }; }; + + buttons { + compatible = "tactility,button-control"; + pin-primary = <&gpio0 0 GPIO_FLAG_NONE>; + }; }; diff --git a/Devices/waveshare-s3-touch-lcd-43/CMakeLists.txt b/Devices/waveshare-s3-touch-lcd-43/CMakeLists.txt index b3db19c78..09a2fdee3 100644 --- a/Devices/waveshare-s3-touch-lcd-43/CMakeLists.txt +++ b/Devices/waveshare-s3-touch-lcd-43/CMakeLists.txt @@ -1,7 +1,7 @@ -file(GLOB_RECURSE SOURCE_FILES Source/*.c*) +file(GLOB_RECURSE SOURCE_FILES source/*.c*) idf_component_register( SRCS ${SOURCE_FILES} - INCLUDE_DIRS "Source" - REQUIRES Tactility esp_lvgl_port RgbDisplay GT911 driver + INCLUDE_DIRS "source" + REQUIRES TactilityKernel ) diff --git a/Devices/waveshare-s3-touch-lcd-43/Source/Configuration.cpp b/Devices/waveshare-s3-touch-lcd-43/Source/Configuration.cpp deleted file mode 100644 index 15dd4828d..000000000 --- a/Devices/waveshare-s3-touch-lcd-43/Source/Configuration.cpp +++ /dev/null @@ -1,30 +0,0 @@ -#include "devices/Display.h" - -#include - -#include - -using namespace tt::hal; - -static bool init() { - auto* i2c = device_find_by_name("i2c0"); - check(i2c); - - uint8_t write_buf = 0x01; - i2c_controller_write(i2c, 0x24, &write_buf, 1, 200 / portTICK_PERIOD_MS); - write_buf = 0x0E; // Pin 2, 3 and 4 high, pin 1 and 5 low - i2c_controller_write(i2c, 0x38, &write_buf, 1, 200 / portTICK_PERIOD_MS); - - return true; -} - -static DeviceVector createDevices() { - return { - createDisplay(), - }; -} - -extern const Configuration hardwareConfiguration = { - .initBoot = init, - .createDevices = createDevices -}; diff --git a/Devices/waveshare-s3-touch-lcd-43/Source/devices/Display.cpp b/Devices/waveshare-s3-touch-lcd-43/Source/devices/Display.cpp deleted file mode 100644 index 30090af1c..000000000 --- a/Devices/waveshare-s3-touch-lcd-43/Source/devices/Display.cpp +++ /dev/null @@ -1,106 +0,0 @@ -#include "Display.h" - -#include -#include -#include -#include - -std::shared_ptr createTouch() { - // Note for future changes: Reset pin is 38 and interrupt pin is 18 - // or INT = NC, schematic and other info floating around is kinda conflicting... - auto* i2c = device_find_by_name("i2c0"); - check(i2c); - auto configuration = std::make_unique( - i2c, - 800, - 480 - ); - - return std::make_shared(std::move(configuration)); -} - -std::shared_ptr createDisplay() { - auto touch = createTouch(); - - constexpr uint32_t bufferPixels = 800 * 10; - - esp_lcd_rgb_panel_config_t rgb_panel_config = { - .clk_src = LCD_CLK_SRC_DEFAULT, - .timings = { - .pclk_hz = 16'000'000, - .h_res = 800, - .v_res = 480, - .hsync_pulse_width = 4, - .hsync_back_porch = 8, - .hsync_front_porch = 8, - .vsync_pulse_width = 4, - .vsync_back_porch = 8, - .vsync_front_porch = 8, - .flags = { - .hsync_idle_low = false, - .vsync_idle_low = false, - .de_idle_high = false, - .pclk_active_neg = true, - .pclk_idle_high = false - } - }, - .data_width = 16, - .bits_per_pixel = 0, - .num_fbs = 2, - .bounce_buffer_size_px = bufferPixels, - .sram_trans_align = 8, - .psram_trans_align = 64, - .hsync_gpio_num = GPIO_NUM_46, - .vsync_gpio_num = GPIO_NUM_3, - .de_gpio_num = GPIO_NUM_5, - .pclk_gpio_num = GPIO_NUM_7, - .disp_gpio_num = GPIO_NUM_NC, - .data_gpio_nums = { - GPIO_NUM_14, // B3 - GPIO_NUM_38, // B4 - GPIO_NUM_18, // B5 - GPIO_NUM_17, // B6 - GPIO_NUM_10, // B7 - GPIO_NUM_39, // G2 - GPIO_NUM_0, // G3 - GPIO_NUM_45, // G4 - GPIO_NUM_48, // G5 - GPIO_NUM_47, // G6 - GPIO_NUM_21, // G7 - GPIO_NUM_1, // R3 - GPIO_NUM_2, // R4 - GPIO_NUM_42, // R5 - GPIO_NUM_41, // R6 - GPIO_NUM_40, // R7 - }, - .flags = { - .disp_active_low = false, - .refresh_on_demand = false, - .fb_in_psram = true, - .double_fb = true, - .no_fb = false, - .bb_invalidate_cache = false - } - }; - - RgbDisplay::BufferConfiguration buffer_config = { - .size = (800 * 480), - .useSpi = true, - .doubleBuffer = true, - .bounceBufferMode = true, - .avoidTearing = false - }; - - auto configuration = std::make_unique( - rgb_panel_config, - buffer_config, - touch, - LV_COLOR_FORMAT_RGB565, - false, - false, - false, - false - ); - - return std::make_shared(std::move(configuration)); -} diff --git a/Devices/waveshare-s3-touch-lcd-43/Source/devices/Display.h b/Devices/waveshare-s3-touch-lcd-43/Source/devices/Display.h deleted file mode 100644 index 5a0d81b38..000000000 --- a/Devices/waveshare-s3-touch-lcd-43/Source/devices/Display.h +++ /dev/null @@ -1,5 +0,0 @@ -#pragma once - -#include "Tactility/hal/display/DisplayDevice.h" - -std::shared_ptr createDisplay(); diff --git a/Devices/waveshare-s3-touch-lcd-43/device.properties b/Devices/waveshare-s3-touch-lcd-43/device.properties index cafbaffdf..c6241af7f 100644 --- a/Devices/waveshare-s3-touch-lcd-43/device.properties +++ b/Devices/waveshare-s3-touch-lcd-43/device.properties @@ -13,6 +13,8 @@ hardware.tinyUsb=true hardware.esptoolFlashFreq=120M hardware.bluetooth=true +dependencies.useDeprecatedHal=false + storage.userDataLocation=SD display.size=4.3" diff --git a/Devices/waveshare-s3-touch-lcd-43/devicetree.yaml b/Devices/waveshare-s3-touch-lcd-43/devicetree.yaml index fa100bdc7..0e44f0a82 100644 --- a/Devices/waveshare-s3-touch-lcd-43/devicetree.yaml +++ b/Devices/waveshare-s3-touch-lcd-43/devicetree.yaml @@ -1,3 +1,6 @@ dependencies: - Platforms/platform-esp32 + - Drivers/ch422g-module + - Drivers/gt911-module + - Drivers/rgb-display-module dts: waveshare,s3-touch-lcd-43.dts diff --git a/Devices/waveshare-s3-touch-lcd-43/Source/module.cpp b/Devices/waveshare-s3-touch-lcd-43/source/module.cpp similarity index 50% rename from Devices/waveshare-s3-touch-lcd-43/Source/module.cpp rename to Devices/waveshare-s3-touch-lcd-43/source/module.cpp index cd0d25ba0..aafff1cb6 100644 --- a/Devices/waveshare-s3-touch-lcd-43/Source/module.cpp +++ b/Devices/waveshare-s3-touch-lcd-43/source/module.cpp @@ -1,21 +1,12 @@ +#include #include extern "C" { -static error_t start() { - // Empty for now - return ERROR_NONE; -} - -static error_t stop() { - // Empty for now - return ERROR_NONE; -} - Module waveshare_s3_touch_lcd_43_module = { .name = "waveshare-s3-touch-lcd-43", - .start = start, - .stop = stop, + .start = [] -> error_t { return ERROR_NONE; }, + .stop = [] -> error_t { return ERROR_NONE; }, .symbols = nullptr, .internal = nullptr }; diff --git a/Devices/waveshare-s3-touch-lcd-43/waveshare,s3-touch-lcd-43.dts b/Devices/waveshare-s3-touch-lcd-43/waveshare,s3-touch-lcd-43.dts index 123bb3ab0..48d10e0e8 100644 --- a/Devices/waveshare-s3-touch-lcd-43/waveshare,s3-touch-lcd-43.dts +++ b/Devices/waveshare-s3-touch-lcd-43/waveshare,s3-touch-lcd-43.dts @@ -8,6 +8,10 @@ #include #include #include +#include +#include +#include +#include / { compatible = "root"; @@ -34,6 +38,36 @@ clock-frequency = <400000>; pin-sda = <&gpio0 8 GPIO_FLAG_PULL_UP>; pin-scl = <&gpio0 9 GPIO_FLAG_PULL_UP>; + + ch422g { + compatible = "wch,ch422g"; + reg = <0x24>; + + pin1_hog { + compatible = "gpio-hog"; + pin = <&ch422g 1 GPIO_FLAG_NONE>; + mode = ; + }; + + pin2_hog { + compatible = "gpio-hog"; + pin = <&ch422g 2 GPIO_FLAG_NONE>; + mode = ; + }; + + pin3_hog { + compatible = "gpio-hog"; + pin = <&ch422g 3 GPIO_FLAG_NONE>; + mode = ; + }; + }; + + touch0 { + compatible = "goodix,gt911"; + reg = <0x5D>; + x-max = <800>; + y-max = <480>; + }; }; spi0 { @@ -50,6 +84,43 @@ }; }; + display0 { + compatible = "espressif,esp32-rgb-display"; + horizontal-resolution = <800>; + vertical-resolution = <480>; + pixel-clock-hz = <16000000>; + hsync-pulse-width = <4>; + hsync-back-porch = <8>; + hsync-front-porch = <8>; + vsync-pulse-width = <4>; + vsync-back-porch = <8>; + vsync-front-porch = <8>; + pclk-active-neg; + num-fbs = <2>; + double-fb; + bounce-buffer-size-px = <8000>; + pin-hsync = <&gpio0 46 GPIO_FLAG_NONE>; + pin-vsync = <&gpio0 3 GPIO_FLAG_NONE>; + pin-de = <&gpio0 5 GPIO_FLAG_NONE>; + pin-pclk = <&gpio0 7 GPIO_FLAG_NONE>; + pin-data0 = <&gpio0 14 GPIO_FLAG_NONE>; // B3 + pin-data1 = <&gpio0 38 GPIO_FLAG_NONE>; // B4 + pin-data2 = <&gpio0 18 GPIO_FLAG_NONE>; // B5 + pin-data3 = <&gpio0 17 GPIO_FLAG_NONE>; // B6 + pin-data4 = <&gpio0 10 GPIO_FLAG_NONE>; // B7 + pin-data5 = <&gpio0 39 GPIO_FLAG_NONE>; // G2 + pin-data6 = <&gpio0 0 GPIO_FLAG_NONE>; // G3 + pin-data7 = <&gpio0 45 GPIO_FLAG_NONE>; // G4 + pin-data8 = <&gpio0 48 GPIO_FLAG_NONE>; // G5 + pin-data9 = <&gpio0 47 GPIO_FLAG_NONE>; // G6 + pin-data10 = <&gpio0 21 GPIO_FLAG_NONE>; // G7 + pin-data11 = <&gpio0 1 GPIO_FLAG_NONE>; // R3 + pin-data12 = <&gpio0 2 GPIO_FLAG_NONE>; // R4 + pin-data13 = <&gpio0 42 GPIO_FLAG_NONE>; // R5 + pin-data14 = <&gpio0 41 GPIO_FLAG_NONE>; // R6 + pin-data15 = <&gpio0 40 GPIO_FLAG_NONE>; // R7 + }; + uart1 { compatible = "espressif,esp32-uart"; port = ; diff --git a/Devices/wireless-tag-wt32-sc01-plus/CMakeLists.txt b/Devices/wireless-tag-wt32-sc01-plus/CMakeLists.txt index 6a7bbba60..09a2fdee3 100644 --- a/Devices/wireless-tag-wt32-sc01-plus/CMakeLists.txt +++ b/Devices/wireless-tag-wt32-sc01-plus/CMakeLists.txt @@ -1,7 +1,7 @@ -file(GLOB_RECURSE SOURCE_FILES Source/*.c*) - -idf_component_register( - SRCS ${SOURCE_FILES} - INCLUDE_DIRS "Source" - REQUIRES Tactility ST7796-i8080 PwmBacklight FT6x36 driver vfs fatfs -) +file(GLOB_RECURSE SOURCE_FILES source/*.c*) + +idf_component_register( + SRCS ${SOURCE_FILES} + INCLUDE_DIRS "source" + REQUIRES TactilityKernel +) diff --git a/Devices/wireless-tag-wt32-sc01-plus/Source/Configuration.cpp b/Devices/wireless-tag-wt32-sc01-plus/Source/Configuration.cpp deleted file mode 100644 index 5cb40ebcf..000000000 --- a/Devices/wireless-tag-wt32-sc01-plus/Source/Configuration.cpp +++ /dev/null @@ -1,21 +0,0 @@ -#include "devices/Display.h" - -#include -#include - -using namespace tt::hal; - -static DeviceVector createDevices() { - return { - createDisplay(), - }; -} - -static bool initBoot() { - return driver::pwmbacklight::init(GPIO_NUM_45); -} - -extern const Configuration hardwareConfiguration = { - .initBoot = initBoot, - .createDevices = createDevices -}; diff --git a/Devices/wireless-tag-wt32-sc01-plus/Source/devices/Display.cpp b/Devices/wireless-tag-wt32-sc01-plus/Source/devices/Display.cpp deleted file mode 100644 index 6176b3244..000000000 --- a/Devices/wireless-tag-wt32-sc01-plus/Source/devices/Display.cpp +++ /dev/null @@ -1,44 +0,0 @@ -#include "Display.h" - -#include -#include -#include - -constexpr auto LCD_HORIZONTAL_RESOLUTION = 320; -constexpr auto LCD_VERTICAL_RESOLUTION = 480; - -std::shared_ptr createTouch() { - auto configuration = std::make_unique( - I2C_NUM_0, - LCD_HORIZONTAL_RESOLUTION, - LCD_VERTICAL_RESOLUTION, - false, - false, - false, - GPIO_NUM_NC, - GPIO_NUM_7 - ); - - return std::make_shared(std::move(configuration)); -} - -std::shared_ptr createDisplay() { - auto configuration = St7796i8080Display::Configuration( - GPIO_NUM_NC, //CS - GPIO_NUM_0, //RS - GPIO_NUM_47, //WR - { GPIO_NUM_9, GPIO_NUM_46, GPIO_NUM_3, GPIO_NUM_8, - GPIO_NUM_18, GPIO_NUM_17, GPIO_NUM_16, GPIO_NUM_15 }, // D0..D7 - GPIO_NUM_4, //RESET - GPIO_NUM_45 //BL - ); - - configuration.mirrorX = true; - configuration.horizontalResolution = LCD_HORIZONTAL_RESOLUTION; - configuration.verticalResolution = LCD_VERTICAL_RESOLUTION; - configuration.touch = createTouch(); - configuration.backlightDutyFunction = driver::pwmbacklight::setBacklightDuty; - - auto display = std::make_shared(configuration); - return display; -} diff --git a/Devices/wireless-tag-wt32-sc01-plus/Source/devices/Display.h b/Devices/wireless-tag-wt32-sc01-plus/Source/devices/Display.h deleted file mode 100644 index c82ab445a..000000000 --- a/Devices/wireless-tag-wt32-sc01-plus/Source/devices/Display.h +++ /dev/null @@ -1,5 +0,0 @@ -#pragma once - -#include - -std::shared_ptr createDisplay(); diff --git a/Devices/wireless-tag-wt32-sc01-plus/Source/module.cpp b/Devices/wireless-tag-wt32-sc01-plus/Source/module.cpp deleted file mode 100644 index 139234a11..000000000 --- a/Devices/wireless-tag-wt32-sc01-plus/Source/module.cpp +++ /dev/null @@ -1,23 +0,0 @@ -#include - -extern "C" { - -static error_t start() { - // Empty for now - return ERROR_NONE; -} - -static error_t stop() { - // Empty for now - return ERROR_NONE; -} - -struct Module wireless_tag_wt32_sc01_plus_module = { - .name = "wireless-tag-wt32-sc01-plus", - .start = start, - .stop = stop, - .symbols = nullptr, - .internal = nullptr -}; - -} diff --git a/Devices/wireless-tag-wt32-sc01-plus/device.properties b/Devices/wireless-tag-wt32-sc01-plus/device.properties index 18f88ac7b..875a0d567 100644 --- a/Devices/wireless-tag-wt32-sc01-plus/device.properties +++ b/Devices/wireless-tag-wt32-sc01-plus/device.properties @@ -12,6 +12,8 @@ hardware.tinyUsb=true hardware.esptoolFlashFreq=80M hardware.bluetooth=true +dependencies.useDeprecatedHal=false + storage.userDataLocation=SD display.size=3.5" diff --git a/Devices/wireless-tag-wt32-sc01-plus/devicetree.yaml b/Devices/wireless-tag-wt32-sc01-plus/devicetree.yaml index 5355dd5c7..4f1b84b33 100644 --- a/Devices/wireless-tag-wt32-sc01-plus/devicetree.yaml +++ b/Devices/wireless-tag-wt32-sc01-plus/devicetree.yaml @@ -1,3 +1,5 @@ dependencies: - Platforms/platform-esp32 + - Drivers/ft6x36-module + - Drivers/st7796-i8080-module dts: wireless-tag,wt32-sc01-plus.dts diff --git a/Devices/wireless-tag-wt32-sc01-plus/source/module.cpp b/Devices/wireless-tag-wt32-sc01-plus/source/module.cpp new file mode 100644 index 000000000..5a028e619 --- /dev/null +++ b/Devices/wireless-tag-wt32-sc01-plus/source/module.cpp @@ -0,0 +1,14 @@ +#include +#include + +extern "C" { + +Module wireless_tag_wt32_sc01_plus_module = { + .name = "wireless-tag-wt32-sc01-plus", + .start = [] -> error_t { return ERROR_NONE; }, + .stop = [] -> error_t { return ERROR_NONE; }, + .symbols = nullptr, + .internal = nullptr +}; + +} diff --git a/Devices/wireless-tag-wt32-sc01-plus/wireless-tag,wt32-sc01-plus.dts b/Devices/wireless-tag-wt32-sc01-plus/wireless-tag,wt32-sc01-plus.dts index c32e41a07..00298196d 100644 --- a/Devices/wireless-tag-wt32-sc01-plus/wireless-tag,wt32-sc01-plus.dts +++ b/Devices/wireless-tag-wt32-sc01-plus/wireless-tag,wt32-sc01-plus.dts @@ -7,6 +7,11 @@ #include #include #include +#include +#include +#include +#include +#include / { compatible = "root"; @@ -33,6 +38,58 @@ clock-frequency = <400000>; pin-sda = <&gpio0 6 GPIO_FLAG_NONE>; pin-scl = <&gpio0 5 GPIO_FLAG_NONE>; + + touch0 { + compatible = "focaltech,ft6x36"; + reg = <0x38>; + x-max = <320>; + y-max = <480>; + pin-interrupt = <&gpio0 7 GPIO_FLAG_NONE>; + }; + }; + + display_backlight_pwm { + compatible = "espressif,esp32-pwm-ledc"; + pin = <&gpio0 45 GPIO_FLAG_NONE>; + period-ns = <25000>; + 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>; + }; + + i8080_0 { + compatible = "espressif,esp32-i8080"; + pin-dc = <&gpio0 0 GPIO_FLAG_NONE>; + pin-wr = <&gpio0 47 GPIO_FLAG_NONE>; + pin-d0 = <&gpio0 9 GPIO_FLAG_NONE>; + pin-d1 = <&gpio0 46 GPIO_FLAG_NONE>; + pin-d2 = <&gpio0 3 GPIO_FLAG_NONE>; + pin-d3 = <&gpio0 8 GPIO_FLAG_NONE>; + pin-d4 = <&gpio0 18 GPIO_FLAG_NONE>; + pin-d5 = <&gpio0 17 GPIO_FLAG_NONE>; + pin-d6 = <&gpio0 16 GPIO_FLAG_NONE>; + pin-d7 = <&gpio0 15 GPIO_FLAG_NONE>; + // horizontal-resolution * vertical-resolution / 10 (partial buffer) * 2 bytes/pixel + max-transfer-bytes = <30720>; + + display@0 { + compatible = "sitronix,st7796-i8080"; + horizontal-resolution = <320>; + vertical-resolution = <480>; + pixel-clock-hz = <20000000>; + mirror-x; + invert-color; + bgr-order; + pin-reset = <&gpio0 4 GPIO_FLAG_NONE>; + backlight = <&display_backlight>; + }; }; spi0 { @@ -42,7 +99,7 @@ pin-mosi = <&gpio0 40 GPIO_FLAG_NONE>; pin-miso = <&gpio0 38 GPIO_FLAG_NONE>; pin-sclk = <&gpio0 39 GPIO_FLAG_NONE>; - + sdcard@0 { compatible = "espressif,esp32-sdspi"; frequency-khz = <20000>; diff --git a/Drivers/axs5106-module/CMakeLists.txt b/Drivers/axs5106-module/CMakeLists.txt new file mode 100644 index 000000000..ca376586f --- /dev/null +++ b/Drivers/axs5106-module/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.20) + +include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake") + +file(GLOB_RECURSE SOURCE_FILES "source/*.c*") + +tactility_add_module(axs5106-module + SRCS ${SOURCE_FILES} + INCLUDE_DIRS include/ + REQUIRES TactilityKernel +) diff --git a/Drivers/axs5106-module/LICENSE-Apache-2.0.md b/Drivers/axs5106-module/LICENSE-Apache-2.0.md new file mode 100644 index 000000000..f5f4b8b5e --- /dev/null +++ b/Drivers/axs5106-module/LICENSE-Apache-2.0.md @@ -0,0 +1,195 @@ +Apache License +============== + +_Version 2.0, January 2004_ +_<>_ + +### Terms and Conditions for use, reproduction, and distribution + +#### 1. Definitions + +“License” shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +“Licensor” shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +“Legal Entity” shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, “control” means **(i)** the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the +outstanding shares, or **(iii)** beneficial ownership of such entity. + +“You” (or “Your”) shall mean an individual or Legal Entity exercising +permissions granted by this License. + +“Source” form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +“Object” form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +“Work” shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +“Derivative Works” shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +“Contribution” shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +“submitted” means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as “Not a Contribution.” + +“Contributor” shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +#### 2. Grant of Copyright License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +#### 3. Grant of Patent License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +#### 4. Redistribution + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +* **(a)** You must give any other recipients of the Work or Derivative Works a copy of +this License; and +* **(b)** You must cause any modified files to carry prominent notices stating that You +changed the files; and +* **(c)** You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. + +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +#### 5. Submission of Contributions + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +#### 6. Trademarks + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +#### 7. Disclaimer of Warranty + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an “AS IS” BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +#### 8. Limitation of Liability + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +#### 9. Accepting Warranty or Additional Liability + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +_END OF TERMS AND CONDITIONS_ + +### APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets `[]` replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same “printed page” as the copyright notice for easier identification within +third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/Drivers/axs5106-module/README.md b/Drivers/axs5106-module/README.md new file mode 100644 index 000000000..dab647127 --- /dev/null +++ b/Drivers/axs5106-module/README.md @@ -0,0 +1,12 @@ +# AXS5106 touch controller + +A driver for the `AXS5106` I2C capacitive touch controller, used on the Waveshare ESP32-S3-Touch-LCD-1.47. + +Ported from the `esp_lcd_touch_axs5106` component bundled with Waveshare's official ESP-IDF factory demo firmware +for this board (`ESP32-S3-Touch-LCD-1.47-Demo/ESP-IDF/01_factory/components/esp_lcd_touch_axs5106/esp_lcd_touch_axs5106.c`, +downloadable from https://www.waveshare.com/wiki/ESP32-S3-Touch-LCD-1.47) - not a standalone repo. +The original license was also Apache v2.0 + +Supports up to 2 touch points. + +License: [Apache v2.0](LICENSE-Apache-2.0.md) diff --git a/Drivers/axs5106-module/bindings/axs,axs5106.yaml b/Drivers/axs5106-module/bindings/axs,axs5106.yaml new file mode 100644 index 000000000..4fe9910ea --- /dev/null +++ b/Drivers/axs5106-module/bindings/axs,axs5106.yaml @@ -0,0 +1,53 @@ +description: AXS5106 capacitive touch controller + +include: ["i2c-device.yaml"] + +compatible: "axs,axs5106" + +bus: i2c + +properties: + x-max: + type: int + required: true + description: Maximum X coordinate reported by the controller (typically the panel's horizontal resolution) + y-max: + type: int + required: true + description: Maximum Y coordinate reported by the controller (typically the panel's vertical resolution) + swap-xy: + type: boolean + default: false + description: Swap the X and Y axes + mirror-x: + type: boolean + default: false + description: Mirror the X axis + mirror-y: + type: boolean + default: false + description: Mirror the Y axis + pin-reset: + type: phandles + default: GPIO_PIN_SPEC_NONE + description: Reset GPIO pin + pin-interrupt: + type: phandles + default: GPIO_PIN_SPEC_NONE + description: Interrupt GPIO pin + reset-active-high: + type: boolean + default: false + description: Whether the reset pin is active high + interrupt-active-high: + type: boolean + default: false + description: Whether the interrupt pin is active high + transmit-timeout: + type: int + default: 100 + description: Timeout (ms) for the register-address write half of a read transaction + receive-timeout: + type: int + default: 100 + description: Timeout (ms) for the data-read half of a read transaction diff --git a/Drivers/axs5106-module/devicetree.yaml b/Drivers/axs5106-module/devicetree.yaml new file mode 100644 index 000000000..a07d6f334 --- /dev/null +++ b/Drivers/axs5106-module/devicetree.yaml @@ -0,0 +1,3 @@ +dependencies: + - TactilityKernel +bindings: bindings diff --git a/Drivers/axs5106-module/include/axs5106_module.h b/Drivers/axs5106-module/include/axs5106_module.h new file mode 100644 index 000000000..3cd7e09c5 --- /dev/null +++ b/Drivers/axs5106-module/include/axs5106_module.h @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +extern struct Module axs5106_module; + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/axs5106-module/include/bindings/axs5106.h b/Drivers/axs5106-module/include/bindings/axs5106.h new file mode 100644 index 000000000..787937d77 --- /dev/null +++ b/Drivers/axs5106-module/include/bindings/axs5106.h @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include + +DEFINE_DEVICETREE(axs5106, struct Axs5106Config) diff --git a/Drivers/axs5106-module/include/drivers/axs5106.h b/Drivers/axs5106-module/include/drivers/axs5106.h new file mode 100644 index 000000000..ac2988fe0 --- /dev/null +++ b/Drivers/axs5106-module/include/drivers/axs5106.h @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include + +struct Axs5106Config { + uint8_t address; + uint16_t x_max; + uint16_t y_max; + bool swap_xy; + bool mirror_x; + bool mirror_y; + struct GpioPinSpec pin_reset; + struct GpioPinSpec pin_interrupt; + bool reset_active_high; + bool interrupt_active_high; + // Timeout (ms) for the register-address write half of a read, and for the data-read half, + // done as two separate I2C transactions - matches the vendor factory demo's + // i2c_master_transmit()/i2c_master_receive() split (each with its own 100ms timeout), + // rather than a single combined write-then-read transaction. + uint32_t transmit_timeout_ms; + uint32_t receive_timeout_ms; +}; + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/axs5106-module/source/axs5106.cpp b/Drivers/axs5106-module/source/axs5106.cpp new file mode 100644 index 000000000..ff3687392 --- /dev/null +++ b/Drivers/axs5106-module/source/axs5106.cpp @@ -0,0 +1,245 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +constexpr auto* TAG = "AXS5106"; +#define GET_CONFIG(device) (static_cast((device)->config)) + +// Register map and read/reset logic ported from the vendor factory demo +// (ESP32-S3-Touch-LCD-1.47-Demo/ESP-IDF/01_factory/components/esp_lcd_touch_axs5106), which talks +// to the chip directly over I2C rather than through esp_lcd_panel_io/esp_lcd_touch - that indirection +// is what the previous version of this driver used, and reads through it consistently failed with +// ESP_FAIL on real hardware. +static constexpr uint8_t REG_TOUCH_POINTS = 0x01; +static constexpr uint8_t TOUCH_DATA_LEN = 14; +static constexpr uint8_t MAX_POINTS = 2; + +struct Axs5106Internal { + GpioDescriptor* reset_descriptor; + bool swap_xy; + bool mirror_x; + bool mirror_y; + uint8_t point_count; + uint16_t x[MAX_POINTS]; + uint16_t y[MAX_POINTS]; +}; + +// region Driver lifecycle + +// Matches the vendor demo's touch_axs5106_reset(): pulse the reset pin active for 10ms, then +// release it for 10ms before any I2C traffic. No separate init step follows - the vendor demo's +// touch_axs5106_init() is a no-op too. +static error_t reset_pulse(GpioDescriptor* descriptor, bool active_high) { + bool idle_level = !active_high; + + bool ok = gpio_descriptor_set_flags(descriptor, GPIO_FLAG_DIRECTION_OUTPUT) == ERROR_NONE; + ok = ok && gpio_descriptor_set_level(descriptor, active_high) == ERROR_NONE; + vTaskDelay(pdMS_TO_TICKS(10)); + ok = ok && gpio_descriptor_set_level(descriptor, idle_level) == ERROR_NONE; + vTaskDelay(pdMS_TO_TICKS(10)); + + return ok ? ERROR_NONE : ERROR_RESOURCE; +} + +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(malloc(sizeof(Axs5106Internal))); + if (internal == nullptr) { + return ERROR_OUT_OF_MEMORY; + } + internal->reset_descriptor = nullptr; + internal->swap_xy = config->swap_xy; + internal->mirror_x = config->mirror_x; + internal->mirror_y = config->mirror_y; + internal->point_count = 0; + + if (config->pin_reset.gpio_controller != nullptr) { + internal->reset_descriptor = gpio_descriptor_acquire(config->pin_reset.gpio_controller, config->pin_reset.pin, GPIO_OWNER_GPIO); + if (internal->reset_descriptor == nullptr) { + LOG_E(TAG, "Failed to acquire reset pin"); + free(internal); + return ERROR_RESOURCE; + } + + if (reset_pulse(internal->reset_descriptor, config->reset_active_high) != ERROR_NONE) { + LOG_E(TAG, "Failed to pulse reset pin"); + gpio_descriptor_release(internal->reset_descriptor); + free(internal); + return ERROR_RESOURCE; + } + } + + device_set_driver_data(device, internal); + return ERROR_NONE; +} + +static error_t stop(Device* device) { + auto* internal = static_cast(device_get_driver_data(device)); + + if (internal->reset_descriptor != nullptr) { + gpio_descriptor_release(internal->reset_descriptor); + } + + free(internal); + device_set_driver_data(device, nullptr); + return ERROR_NONE; +} + +// endregion + +// region PointerApi + +static error_t axs5106_read_data(Device* device, TickType_t) { + auto* internal = static_cast(device_get_driver_data(device)); + const auto* config = GET_CONFIG(device); + auto* parent = device_get_parent(device); + + // Matches the vendor demo's touch_axs5106_i2c_read(): the register address write and the + // data read are two separate transactions (i2c_master_transmit() then i2c_master_receive()), + // not a single write-then-read transaction with a repeated start. + uint8_t reg = REG_TOUCH_POINTS; + error_t error = i2c_controller_write(parent, config->address, ®, 1, pdMS_TO_TICKS(config->transmit_timeout_ms)); + if (error != ERROR_NONE) { + return error; + } + + uint8_t data[TOUCH_DATA_LEN]; + error = i2c_controller_read(parent, config->address, data, sizeof(data), pdMS_TO_TICKS(config->receive_timeout_ms)); + if (error != ERROR_NONE) { + return error; + } + + uint8_t points = data[1] & 0x0F; + if (points > MAX_POINTS) { + points = MAX_POINTS; + } + internal->point_count = points; + + for (uint8_t i = 0; i < points; i++) { + internal->y[i] = static_cast(((data[4 + i * 6] & 0x0F) << 8) | data[5 + i * 6]); + internal->x[i] = static_cast(((data[2 + i * 6] & 0x0F) << 8) | data[3 + i * 6]); + } + + return ERROR_NONE; +} + +static bool axs5106_get_touched_points(Device* device, uint16_t* x, uint16_t* y, uint16_t* strength, uint8_t* point_count, uint8_t max_point_count) { + auto* internal = static_cast(device_get_driver_data(device)); + const auto* config = GET_CONFIG(device); + + uint8_t count = internal->point_count; + if (count > max_point_count) { + count = max_point_count; + } + + // Order matches esp_lcd_touch's own software coordinate adjustment (mirror X, mirror Y, + // then swap) so behavior lines up with the other touch modules in this codebase. + for (uint8_t i = 0; i < count; i++) { + uint16_t point_x = internal->x[i]; + uint16_t point_y = internal->y[i]; + if (internal->mirror_x) { + point_x = config->x_max - point_x; + } + if (internal->mirror_y) { + point_y = config->y_max - point_y; + } + if (internal->swap_xy) { + uint16_t tmp = point_x; + point_x = point_y; + point_y = tmp; + } + x[i] = point_x; + y[i] = point_y; + if (strength != nullptr) { + strength[i] = 0; + } + + LOG_I(TAG, "%d, %d", point_x, point_y); + } + + *point_count = count; + return count > 0; +} + +static error_t axs5106_set_swap_xy(Device* device, bool swap) { + auto* internal = static_cast(device_get_driver_data(device)); + internal->swap_xy = swap; + return ERROR_NONE; +} + +static error_t axs5106_get_swap_xy(Device* device, bool* swap) { + auto* internal = static_cast(device_get_driver_data(device)); + *swap = internal->swap_xy; + return ERROR_NONE; +} + +static error_t axs5106_set_mirror_x(Device* device, bool mirror) { + auto* internal = static_cast(device_get_driver_data(device)); + internal->mirror_x = mirror; + return ERROR_NONE; +} + +static error_t axs5106_get_mirror_x(Device* device, bool* mirror) { + auto* internal = static_cast(device_get_driver_data(device)); + *mirror = internal->mirror_x; + return ERROR_NONE; +} + +static error_t axs5106_set_mirror_y(Device* device, bool mirror) { + auto* internal = static_cast(device_get_driver_data(device)); + internal->mirror_y = mirror; + return ERROR_NONE; +} + +static error_t axs5106_get_mirror_y(Device* device, bool* mirror) { + auto* internal = static_cast(device_get_driver_data(device)); + *mirror = internal->mirror_y; + return ERROR_NONE; +} + +// endregion + +static const PointerApi axs5106_pointer_api = { + // Unsupported (null per PointerApi's contract): neither the vendor demo nor the original + // esp_lcd_touch component implements a sleep mode for this chip. + .enter_sleep = nullptr, + .exit_sleep = nullptr, + .read_data = axs5106_read_data, + .get_touched_points = axs5106_get_touched_points, + .set_swap_xy = axs5106_set_swap_xy, + .get_swap_xy = axs5106_get_swap_xy, + .set_mirror_x = axs5106_set_mirror_x, + .get_mirror_x = axs5106_get_mirror_x, + .set_mirror_y = axs5106_set_mirror_y, + .get_mirror_y = axs5106_get_mirror_y, +}; + +Driver axs5106_driver = { + .name = "axs5106", + .compatible = (const char*[]) { "axs,axs5106", nullptr }, + .start_device = start, + .stop_device = stop, + .api = &axs5106_pointer_api, + .device_type = &POINTER_TYPE, + .owner = &axs5106_module, + .internal = nullptr +}; diff --git a/Drivers/axs5106-module/source/module.cpp b/Drivers/axs5106-module/source/module.cpp new file mode 100644 index 000000000..9d92a496b --- /dev/null +++ b/Drivers/axs5106-module/source/module.cpp @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include + +extern "C" { + +extern Driver axs5106_driver; + +static error_t start() { + /* We crash when construct fails, because if a single driver fails to construct, + * there is no guarantee that the previously constructed drivers can be destroyed */ + check(driver_construct_add(&axs5106_driver) == ERROR_NONE); + return ERROR_NONE; +} + +static error_t stop() { + /* We crash when destruct fails, because if a single driver fails to destruct, + * there is no guarantee that the previously destroyed drivers can be recovered */ + check(driver_remove_destruct(&axs5106_driver) == ERROR_NONE); + return ERROR_NONE; +} + +Module axs5106_module = { + .name = "axs5106", + .start = start, + .stop = stop, + .symbols = nullptr, + .internal = nullptr +}; + +} // extern "C" diff --git a/Drivers/ch422g-module/CMakeLists.txt b/Drivers/ch422g-module/CMakeLists.txt new file mode 100644 index 000000000..2d0e99dfe --- /dev/null +++ b/Drivers/ch422g-module/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.20) + +include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake") + +file(GLOB_RECURSE SOURCE_FILES "source/*.c*") + +tactility_add_module(ch422g-module + SRCS ${SOURCE_FILES} + INCLUDE_DIRS include/ + REQUIRES TactilityKernel +) diff --git a/Drivers/ch422g-module/LICENSE-Apache-2.0.md b/Drivers/ch422g-module/LICENSE-Apache-2.0.md new file mode 100644 index 000000000..f5f4b8b5e --- /dev/null +++ b/Drivers/ch422g-module/LICENSE-Apache-2.0.md @@ -0,0 +1,195 @@ +Apache License +============== + +_Version 2.0, January 2004_ +_<>_ + +### Terms and Conditions for use, reproduction, and distribution + +#### 1. Definitions + +“License” shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +“Licensor” shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +“Legal Entity” shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, “control” means **(i)** the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the +outstanding shares, or **(iii)** beneficial ownership of such entity. + +“You” (or “Your”) shall mean an individual or Legal Entity exercising +permissions granted by this License. + +“Source” form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +“Object” form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +“Work” shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +“Derivative Works” shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +“Contribution” shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +“submitted” means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as “Not a Contribution.” + +“Contributor” shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +#### 2. Grant of Copyright License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +#### 3. Grant of Patent License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +#### 4. Redistribution + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +* **(a)** You must give any other recipients of the Work or Derivative Works a copy of +this License; and +* **(b)** You must cause any modified files to carry prominent notices stating that You +changed the files; and +* **(c)** You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. + +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +#### 5. Submission of Contributions + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +#### 6. Trademarks + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +#### 7. Disclaimer of Warranty + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an “AS IS” BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +#### 8. Limitation of Liability + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +#### 9. Accepting Warranty or Additional Liability + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +_END OF TERMS AND CONDITIONS_ + +### APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets `[]` replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same “printed page” as the copyright notice for easier identification within +third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/Drivers/ch422g-module/README.md b/Drivers/ch422g-module/README.md new file mode 100644 index 000000000..ebba1cc10 --- /dev/null +++ b/Drivers/ch422g-module/README.md @@ -0,0 +1,10 @@ +# CH422G I/O expander + +A driver for the `CH422G` I2C-bus I/O expander by WCH, commonly used on Waveshare ESP32 +display boards to drive display/touch reset and enable lines. + +Exposes EXIO0-7 as a `GPIO_CONTROLLER_TYPE` device. The chip has no register to configure +per-pin direction and no readback for its output register, so only output pins are supported +and the driver keeps a local shadow of the last written value. + +License: [Apache v2.0](LICENSE-Apache-2.0.md) diff --git a/Drivers/ch422g-module/bindings/wch,ch422g.yaml b/Drivers/ch422g-module/bindings/wch,ch422g.yaml new file mode 100644 index 000000000..c989f596d --- /dev/null +++ b/Drivers/ch422g-module/bindings/wch,ch422g.yaml @@ -0,0 +1,8 @@ +description: > + WCH CH422G I2C IO expander. Exposes EXIO0-7 as a GPIO_CONTROLLER_TYPE device. The chip + has no per-pin direction register and no readback for its output register, so only + output pins are supported; the driver keeps a local shadow of the last written value. + +include: ["i2c-device.yaml"] + +compatible: "wch,ch422g" diff --git a/Drivers/ch422g-module/devicetree.yaml b/Drivers/ch422g-module/devicetree.yaml new file mode 100644 index 000000000..a07d6f334 --- /dev/null +++ b/Drivers/ch422g-module/devicetree.yaml @@ -0,0 +1,3 @@ +dependencies: + - TactilityKernel +bindings: bindings diff --git a/Drivers/ch422g-module/include/bindings/ch422g.h b/Drivers/ch422g-module/include/bindings/ch422g.h new file mode 100644 index 000000000..0fdac0bf7 --- /dev/null +++ b/Drivers/ch422g-module/include/bindings/ch422g.h @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +DEFINE_DEVICETREE(ch422g, struct Ch422gConfig) + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/ch422g-module/include/ch422g_module.h b/Drivers/ch422g-module/include/ch422g_module.h new file mode 100644 index 000000000..2897294a4 --- /dev/null +++ b/Drivers/ch422g-module/include/ch422g_module.h @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +extern struct Module ch422g_module; + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/ch422g-module/include/drivers/ch422g.h b/Drivers/ch422g-module/include/drivers/ch422g.h new file mode 100644 index 000000000..1d72ecdc1 --- /dev/null +++ b/Drivers/ch422g-module/include/drivers/ch422g.h @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +struct Ch422gConfig { + /** Address of the WR_SET (system config) pseudo-register. RD_IO and WR_IO are separate + * fixed pseudo-addresses (the chip has no address-select pins, so this is always 0x24). */ + uint8_t address; +}; + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/ch422g-module/source/ch422g.cpp b/Drivers/ch422g-module/source/ch422g.cpp new file mode 100644 index 000000000..43e284a6f --- /dev/null +++ b/Drivers/ch422g-module/source/ch422g.cpp @@ -0,0 +1,163 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +constexpr auto* TAG = "CH422G"; + +#define GET_CONFIG(device) (static_cast((device)->config)) + +// The chip has no address-select pins: RD_IO and WR_IO are always at these fixed +// pseudo-addresses, regardless of what's configured as the WR_SET (system config) address. +constexpr uint8_t CH422G_ADDR_RD_IO = 0x26; +constexpr uint8_t CH422G_ADDR_WR_IO = 0x38; + +// WR_SET bit that switches EXIO0-7 into GPIO output mode. +constexpr uint8_t CH422G_SYSTEM_CONFIG_IO_OE = 0x01; + +struct Ch422gContext { + Mutex mutex {}; + // WR_IO has no readback, so the last written value is tracked here for read-modify-write. + uint8_t output_shadow = 0x00; +}; + +static error_t start(Device* device) { + auto* parent = device_get_parent(device); + if (device_get_type(parent) != &I2C_CONTROLLER_TYPE) { + LOG_E(TAG, "Parent device is not I2C"); + return ERROR_RESOURCE; + } + + uint8_t wr_set = CH422G_SYSTEM_CONFIG_IO_OE; + if (i2c_controller_write(parent, GET_CONFIG(device)->address, &wr_set, 1, portMAX_DELAY) != ERROR_NONE) { + LOG_E(TAG, "Failed to configure system register"); + return ERROR_RESOURCE; + } + + auto* context = new(std::nothrow) Ch422gContext(); + if (context == nullptr) { + return ERROR_OUT_OF_MEMORY; + } + mutex_construct(&context->mutex); + + // Drive all outputs low, matching the shadow's initial value. + if (i2c_controller_write(parent, CH422G_ADDR_WR_IO, &context->output_shadow, 1, portMAX_DELAY) != ERROR_NONE) { + LOG_W(TAG, "Failed to initialize outputs (non-fatal)"); + } + + error_t error = gpio_controller_init_descriptors(device, 8, context); + if (error != ERROR_NONE) { + mutex_destruct(&context->mutex); + delete context; + return error; + } + + return ERROR_NONE; +} + +static error_t stop(Device* device) { + auto* context = static_cast(gpio_controller_get_controller_context(device)); + check(gpio_controller_deinit_descriptors(device) == ERROR_NONE); + mutex_destruct(&context->mutex); + delete context; + return ERROR_NONE; +} + +extern "C" { + +static error_t set_level(GpioDescriptor* descriptor, bool high) { + auto* context = static_cast(descriptor->controller_context); + auto* parent = device_get_parent(descriptor->controller); + uint8_t bit = 1 << descriptor->pin; + + mutex_lock(&context->mutex); + uint8_t new_shadow = high ? (context->output_shadow | bit) : static_cast(context->output_shadow & ~bit); + error_t err = i2c_controller_write(parent, CH422G_ADDR_WR_IO, &new_shadow, 1, portMAX_DELAY); + if (err == ERROR_NONE) { + context->output_shadow = new_shadow; + } + mutex_unlock(&context->mutex); + + return err; +} + +static error_t get_level(GpioDescriptor* descriptor, bool* high) { + auto* parent = device_get_parent(descriptor->controller); + uint8_t bits; + + error_t err = i2c_controller_read(parent, CH422G_ADDR_RD_IO, &bits, 1, portMAX_DELAY); + if (err != ERROR_NONE) { + return err; + } + + *high = (bits & (1 << descriptor->pin)) != 0; + return ERROR_NONE; +} + +static error_t set_flags(GpioDescriptor*, gpio_flags_t flags) { + // The chip has no per-pin direction register: EXIO0-7 are outputs once WR_SET enables + // IO_OE. Pull-up/down and high-impedance (true input mode) are not present either. + if (flags & (GPIO_FLAG_DIRECTION_INPUT | GPIO_FLAG_PULL_UP | GPIO_FLAG_PULL_DOWN | GPIO_FLAG_HIGH_IMPEDANCE)) { + return ERROR_NOT_SUPPORTED; + } + return ERROR_NONE; +} + +static error_t get_flags(GpioDescriptor*, gpio_flags_t* flags) { + *flags = GPIO_FLAG_DIRECTION_OUTPUT; + return ERROR_NONE; +} + +static error_t get_native_pin_number(GpioDescriptor*, void*) { + return ERROR_NOT_SUPPORTED; +} + +static error_t add_callback(GpioDescriptor*, void (*)(void*), void*) { + return ERROR_NOT_SUPPORTED; +} + +static error_t remove_callback(GpioDescriptor*) { + return ERROR_NOT_SUPPORTED; +} + +static error_t enable_interrupt(GpioDescriptor*) { + return ERROR_NOT_SUPPORTED; +} + +static error_t disable_interrupt(GpioDescriptor*) { + return ERROR_NOT_SUPPORTED; +} + +const static GpioControllerApi ch422g_gpio_api = { + .set_level = set_level, + .get_level = get_level, + .set_flags = set_flags, + .get_flags = get_flags, + .get_native_pin_number = get_native_pin_number, + .add_callback = add_callback, + .remove_callback = remove_callback, + .enable_interrupt = enable_interrupt, + .disable_interrupt = disable_interrupt +}; + +Driver ch422g_driver = { + .name = "ch422g", + .compatible = (const char*[]) { "wch,ch422g", nullptr }, + .start_device = start, + .stop_device = stop, + .api = static_cast(&ch422g_gpio_api), + .device_type = &GPIO_CONTROLLER_TYPE, + .owner = &ch422g_module, + .internal = nullptr +}; + +} diff --git a/Drivers/ch422g-module/source/module.cpp b/Drivers/ch422g-module/source/module.cpp new file mode 100644 index 000000000..c22c90097 --- /dev/null +++ b/Drivers/ch422g-module/source/module.cpp @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include + +extern "C" { + +extern Driver ch422g_driver; + +static error_t start() { + /* We crash when construct fails, because if a single driver fails to construct, + * there is no guarantee that the previously constructed drivers can be destroyed */ + check(driver_construct_add(&ch422g_driver) == ERROR_NONE); + return ERROR_NONE; +} + +static error_t stop() { + /* We crash when destruct fails, because if a single driver fails to destruct, + * there is no guarantee that the previously destroyed drivers can be recovered */ + check(driver_remove_destruct(&ch422g_driver) == ERROR_NONE); + return ERROR_NONE; +} + +Module ch422g_module = { + .name = "ch422g", + .start = start, + .stop = stop, + .symbols = nullptr, + .internal = nullptr +}; + +} diff --git a/Drivers/cst816t-module/CMakeLists.txt b/Drivers/cst816t-module/CMakeLists.txt new file mode 100644 index 000000000..b9667bf04 --- /dev/null +++ b/Drivers/cst816t-module/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.20) + +include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake") + +file(GLOB_RECURSE SOURCE_FILES "source/*.c*") + +tactility_add_module(cst816t-module + SRCS ${SOURCE_FILES} + INCLUDE_DIRS include/ + REQUIRES TactilityKernel +) diff --git a/Drivers/cst816t-module/LICENSE-Apache-2.0.md b/Drivers/cst816t-module/LICENSE-Apache-2.0.md new file mode 100644 index 000000000..f5f4b8b5e --- /dev/null +++ b/Drivers/cst816t-module/LICENSE-Apache-2.0.md @@ -0,0 +1,195 @@ +Apache License +============== + +_Version 2.0, January 2004_ +_<>_ + +### Terms and Conditions for use, reproduction, and distribution + +#### 1. Definitions + +“License” shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +“Licensor” shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +“Legal Entity” shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, “control” means **(i)** the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the +outstanding shares, or **(iii)** beneficial ownership of such entity. + +“You” (or “Your”) shall mean an individual or Legal Entity exercising +permissions granted by this License. + +“Source” form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +“Object” form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +“Work” shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +“Derivative Works” shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +“Contribution” shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +“submitted” means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as “Not a Contribution.” + +“Contributor” shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +#### 2. Grant of Copyright License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +#### 3. Grant of Patent License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +#### 4. Redistribution + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +* **(a)** You must give any other recipients of the Work or Derivative Works a copy of +this License; and +* **(b)** You must cause any modified files to carry prominent notices stating that You +changed the files; and +* **(c)** You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. + +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +#### 5. Submission of Contributions + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +#### 6. Trademarks + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +#### 7. Disclaimer of Warranty + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an “AS IS” BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +#### 8. Limitation of Liability + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +#### 9. Accepting Warranty or Additional Liability + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +_END OF TERMS AND CONDITIONS_ + +### APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets `[]` replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same “printed page” as the copyright notice for easier identification within +third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/Drivers/cst816t-module/README.md b/Drivers/cst816t-module/README.md new file mode 100644 index 000000000..d8b8a3c9d --- /dev/null +++ b/Drivers/cst816t-module/README.md @@ -0,0 +1,16 @@ +# CST816T touch controller + +A driver for the `CST816T` I2C capacitive touch controller by Hynitron. + +Implemented directly against the register. (no `esp_lcd_touch` dependency) + +- Manufacturer datasheet: https://github.com/koendv/cst816t/blob/master/extras/DS-CST816T%E6%95%B0%E6%8D%AE%E6%89%8B%E5%86%8CV2.2.pdf +- Original/reference code: https://github.com/koendv/cst816t (Arduino driver, `src/cst816t.cpp`/`src/cst816t.h`), licensed under [The Unlicense](https://github.com/koendv/cst816t/blob/master/LICENSE) + +Single-touch only: the datasheet advertises "genuine two-point" support, but neither of the sources above documents a second touch-point register set, so only one point is read. + +`enter_sleep`/`exit_sleep` are unimplemented (`nullptr` in the `PointerApi`, per its "null if not supported" contract). +The reference driver defines `REG_SLEEP_MODE` (0xE5) but never writes it, and the datasheet says waking from sleep needs +a hardware reset pulse anyway, not a register write. + +License: [Apache v2.0](LICENSE-Apache-2.0.md) diff --git a/Drivers/cst816t-module/bindings/hynitron,cst816t.yaml b/Drivers/cst816t-module/bindings/hynitron,cst816t.yaml new file mode 100644 index 000000000..217968231 --- /dev/null +++ b/Drivers/cst816t-module/bindings/hynitron,cst816t.yaml @@ -0,0 +1,45 @@ +description: Hynitron CST816T capacitive touch controller + +include: ["i2c-device.yaml"] + +compatible: "hynitron,cst816t" + +bus: i2c + +properties: + x-max: + type: int + required: true + description: Maximum X coordinate reported by the controller (typically the panel's horizontal resolution) + y-max: + type: int + required: true + description: Maximum Y coordinate reported by the controller (typically the panel's vertical resolution) + swap-xy: + type: boolean + default: false + description: Swap the X and Y axes + mirror-x: + type: boolean + default: false + description: Mirror the X axis + mirror-y: + type: boolean + default: false + description: Mirror the Y axis + pin-reset: + type: phandles + default: GPIO_PIN_SPEC_NONE + description: Reset GPIO pin + pin-interrupt: + type: phandles + default: GPIO_PIN_SPEC_NONE + description: Interrupt GPIO pin (currently unused - reads are polled via read_data()) + reset-active-high: + type: boolean + default: false + description: Whether the reset pin is active high + interrupt-active-high: + type: boolean + default: false + description: Whether the interrupt pin is active high diff --git a/Drivers/cst816t-module/devicetree.yaml b/Drivers/cst816t-module/devicetree.yaml new file mode 100644 index 000000000..a07d6f334 --- /dev/null +++ b/Drivers/cst816t-module/devicetree.yaml @@ -0,0 +1,3 @@ +dependencies: + - TactilityKernel +bindings: bindings diff --git a/Drivers/cst816t-module/include/bindings/cst816t.h b/Drivers/cst816t-module/include/bindings/cst816t.h new file mode 100644 index 000000000..dba0d3461 --- /dev/null +++ b/Drivers/cst816t-module/include/bindings/cst816t.h @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include + +DEFINE_DEVICETREE(cst816t, struct Cst816tConfig) diff --git a/Drivers/cst816t-module/include/cst816t_module.h b/Drivers/cst816t-module/include/cst816t_module.h new file mode 100644 index 000000000..aaa7fc2e3 --- /dev/null +++ b/Drivers/cst816t-module/include/cst816t_module.h @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +extern struct Module cst816t_module; + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/cst816t-module/include/drivers/cst816t.h b/Drivers/cst816t-module/include/drivers/cst816t.h new file mode 100644 index 000000000..b85480bff --- /dev/null +++ b/Drivers/cst816t-module/include/drivers/cst816t.h @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include + +struct Cst816tConfig { + uint8_t address; + uint16_t x_max; + uint16_t y_max; + bool swap_xy; + bool mirror_x; + bool mirror_y; + struct GpioPinSpec pin_reset; + struct GpioPinSpec pin_interrupt; + bool reset_active_high; + bool interrupt_active_high; +}; + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/cst816t-module/source/cst816t.cpp b/Drivers/cst816t-module/source/cst816t.cpp new file mode 100644 index 000000000..d9f29ddac --- /dev/null +++ b/Drivers/cst816t-module/source/cst816t.cpp @@ -0,0 +1,254 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +constexpr auto* TAG = "CST816T"; +#define GET_CONFIG(device) (static_cast((device)->config)) + +// Register map and reset/bring-up sequence taken from https://github.com/koendv/cst816t +// (src/cst816t.cpp) - the manufacturer datasheet (DS-CST816T V2.2) covers electrical +// characteristics and I2C timing only, not the register map. +static constexpr uint8_t REG_GESTURE_ID = 0x01; +static constexpr uint8_t REG_CHIP_ID = 0xA7; +static constexpr uint8_t REG_DIS_AUTOSLEEP = 0xFE; + +static constexpr TickType_t TIMEOUT = pdMS_TO_TICKS(50); + +struct Cst816tInternal { + GpioDescriptor* reset_descriptor; + bool swap_xy; + bool mirror_x; + bool mirror_y; + bool touched; + uint16_t x; + uint16_t y; +}; + +// region Driver lifecycle + +// Timings match the reference driver linked above: it holds RST high for 100ms (well past the +// datasheet's Tpor/Tron minimum of 100ms), pulses it low for 10ms (Trst minimum is 0.1ms), then +// waits another 100ms (Tron) for the chip to finish reinitializing before any I2C traffic. +static error_t reset_pulse(GpioDescriptor* descriptor, bool active_high) { + bool idle_level = !active_high; + + bool ok = gpio_descriptor_set_flags(descriptor, GPIO_FLAG_DIRECTION_OUTPUT) == ERROR_NONE; + ok = ok && gpio_descriptor_set_level(descriptor, idle_level) == ERROR_NONE; + vTaskDelay(pdMS_TO_TICKS(100)); + ok = ok && gpio_descriptor_set_level(descriptor, active_high) == ERROR_NONE; + vTaskDelay(pdMS_TO_TICKS(10)); + ok = ok && gpio_descriptor_set_level(descriptor, idle_level) == ERROR_NONE; + vTaskDelay(pdMS_TO_TICKS(100)); + + return ok ? ERROR_NONE : ERROR_RESOURCE; +} + +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(malloc(sizeof(Cst816tInternal))); + if (internal == nullptr) { + return ERROR_OUT_OF_MEMORY; + } + internal->reset_descriptor = nullptr; + internal->swap_xy = config->swap_xy; + internal->mirror_x = config->mirror_x; + internal->mirror_y = config->mirror_y; + internal->touched = false; + internal->x = 0; + internal->y = 0; + + if (config->pin_reset.gpio_controller != nullptr) { + internal->reset_descriptor = gpio_descriptor_acquire(config->pin_reset.gpio_controller, config->pin_reset.pin, GPIO_OWNER_GPIO); + if (internal->reset_descriptor == nullptr) { + LOG_E(TAG, "Failed to acquire reset pin"); + free(internal); + return ERROR_RESOURCE; + } + + if (reset_pulse(internal->reset_descriptor, config->reset_active_high) != ERROR_NONE) { + LOG_E(TAG, "Failed to pulse reset pin"); + gpio_descriptor_release(internal->reset_descriptor); + free(internal); + return ERROR_RESOURCE; + } + } + + uint8_t chip_id = 0; + if (i2c_controller_register8_get(parent, config->address, REG_CHIP_ID, &chip_id, TIMEOUT) != ERROR_NONE) { + LOG_E(TAG, "Failed to read chip ID"); + if (internal->reset_descriptor != nullptr) { + gpio_descriptor_release(internal->reset_descriptor); + } + free(internal); + return ERROR_RESOURCE; + } + LOG_I(TAG, "Chip ID 0x%02X", chip_id); + + // Auto-sleep would otherwise drop the controller into standby after ~2s of inactivity, + // adding wake-up latency to the next touch; disabled since exit_sleep() isn't implemented + // here (see README) so there'd be no way back out once it kicked in. + uint8_t dis_auto_sleep = 0xFF; + if (i2c_controller_register8_set(parent, config->address, REG_DIS_AUTOSLEEP, dis_auto_sleep, TIMEOUT) != ERROR_NONE) { + LOG_W(TAG, "Failed to disable auto-sleep (non-fatal)"); + } + + device_set_driver_data(device, internal); + return ERROR_NONE; +} + +static error_t stop(Device* device) { + auto* internal = static_cast(device_get_driver_data(device)); + + if (internal->reset_descriptor != nullptr) { + gpio_descriptor_release(internal->reset_descriptor); + } + + free(internal); + device_set_driver_data(device, nullptr); + return ERROR_NONE; +} + +// endregion + +// region PointerApi + +static error_t cst816t_read_data(Device* device, TickType_t) { + auto* internal = static_cast(device_get_driver_data(device)); + const auto* config = GET_CONFIG(device); + auto* parent = device_get_parent(device); + + // Burst-reads gesture_id, finger_num and X/Y in one transaction, matching the reference + // driver: the chip presents them as one contiguous 6-byte block starting at REG_GESTURE_ID. + uint8_t data[6]; + error_t error = i2c_controller_read_register(parent, config->address, REG_GESTURE_ID, data, sizeof(data), TIMEOUT); + if (error != ERROR_NONE) { + return error; + } + + uint8_t finger_num = data[1]; + internal->touched = finger_num > 0; + // Top nibble of the high byte is reserved/event-type; the coordinate itself is 12-bit. + internal->x = static_cast(((data[2] & 0x0F) << 8) | data[3]); + internal->y = static_cast(((data[4] & 0x0F) << 8) | data[5]); + + return ERROR_NONE; +} + +// Single-touch only - see README for why. +static bool cst816t_get_touched_points(Device* device, uint16_t* x, uint16_t* y, uint16_t* strength, uint8_t* point_count, uint8_t max_point_count) { + auto* internal = static_cast(device_get_driver_data(device)); + const auto* config = GET_CONFIG(device); + + if (!internal->touched || max_point_count == 0) { + *point_count = 0; + return false; + } + + // Order matches esp_lcd_touch's own software coordinate adjustment (mirror X, mirror Y, + // then swap) so behavior lines up with the other touch modules in this codebase. + uint16_t point_x = internal->x; + uint16_t point_y = internal->y; + if (internal->mirror_x) { + point_x = config->x_max - point_x; + } + if (internal->mirror_y) { + point_y = config->y_max - point_y; + } + if (internal->swap_xy) { + uint16_t tmp = point_x; + point_x = point_y; + point_y = tmp; + } + + x[0] = point_x; + y[0] = point_y; + if (strength != nullptr) { + strength[0] = 0; + } + *point_count = 1; + return true; +} + +static error_t cst816t_set_swap_xy(Device* device, bool swap) { + auto* internal = static_cast(device_get_driver_data(device)); + internal->swap_xy = swap; + return ERROR_NONE; +} + +static error_t cst816t_get_swap_xy(Device* device, bool* swap) { + auto* internal = static_cast(device_get_driver_data(device)); + *swap = internal->swap_xy; + return ERROR_NONE; +} + +static error_t cst816t_set_mirror_x(Device* device, bool mirror) { + auto* internal = static_cast(device_get_driver_data(device)); + internal->mirror_x = mirror; + return ERROR_NONE; +} + +static error_t cst816t_get_mirror_x(Device* device, bool* mirror) { + auto* internal = static_cast(device_get_driver_data(device)); + *mirror = internal->mirror_x; + return ERROR_NONE; +} + +static error_t cst816t_set_mirror_y(Device* device, bool mirror) { + auto* internal = static_cast(device_get_driver_data(device)); + internal->mirror_y = mirror; + return ERROR_NONE; +} + +static error_t cst816t_get_mirror_y(Device* device, bool* mirror) { + auto* internal = static_cast(device_get_driver_data(device)); + *mirror = internal->mirror_y; + return ERROR_NONE; +} + +// endregion + +static const PointerApi cst816t_pointer_api = { + // Unsupported (null per PointerApi's contract): the reference driver defines + // REG_SLEEP_MODE (0xE5) but never writes it, and the datasheet says waking up needs a + // hardware reset pulse rather than a register write. + .enter_sleep = nullptr, + .exit_sleep = nullptr, + .read_data = cst816t_read_data, + .get_touched_points = cst816t_get_touched_points, + .set_swap_xy = cst816t_set_swap_xy, + .get_swap_xy = cst816t_get_swap_xy, + .set_mirror_x = cst816t_set_mirror_x, + .get_mirror_x = cst816t_get_mirror_x, + .set_mirror_y = cst816t_set_mirror_y, + .get_mirror_y = cst816t_get_mirror_y, +}; + +Driver cst816t_driver = { + .name = "cst816t", + .compatible = (const char*[]) { "hynitron,cst816t", nullptr }, + .start_device = start, + .stop_device = stop, + .api = &cst816t_pointer_api, + .device_type = &POINTER_TYPE, + .owner = &cst816t_module, + .internal = nullptr +}; diff --git a/Drivers/cst816t-module/source/module.cpp b/Drivers/cst816t-module/source/module.cpp new file mode 100644 index 000000000..50cb3c15e --- /dev/null +++ b/Drivers/cst816t-module/source/module.cpp @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include + +extern "C" { + +extern Driver cst816t_driver; + +static error_t start() { + return driver_construct_add(&cst816t_driver); +} + +static error_t stop() { + return driver_remove_destruct(&cst816t_driver); +} + +Module cst816t_module = { + .name = "cst816t", + .start = start, + .stop = stop, + .symbols = nullptr, + .internal = nullptr +}; + +} // extern "C" diff --git a/Drivers/gc9a01-module/CMakeLists.txt b/Drivers/gc9a01-module/CMakeLists.txt new file mode 100644 index 000000000..02735fb05 --- /dev/null +++ b/Drivers/gc9a01-module/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.20) + +include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake") + +file(GLOB_RECURSE SOURCE_FILES "source/*.c*") + +tactility_add_module(gc9a01-module + SRCS ${SOURCE_FILES} + INCLUDE_DIRS include/ + REQUIRES TactilityKernel platform-esp32 esp_lcd_gc9a01 driver +) diff --git a/Drivers/gc9a01-module/LICENSE-Apache-2.0.md b/Drivers/gc9a01-module/LICENSE-Apache-2.0.md new file mode 100644 index 000000000..f5f4b8b5e --- /dev/null +++ b/Drivers/gc9a01-module/LICENSE-Apache-2.0.md @@ -0,0 +1,195 @@ +Apache License +============== + +_Version 2.0, January 2004_ +_<>_ + +### Terms and Conditions for use, reproduction, and distribution + +#### 1. Definitions + +“License” shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +“Licensor” shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +“Legal Entity” shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, “control” means **(i)** the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the +outstanding shares, or **(iii)** beneficial ownership of such entity. + +“You” (or “Your”) shall mean an individual or Legal Entity exercising +permissions granted by this License. + +“Source” form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +“Object” form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +“Work” shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +“Derivative Works” shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +“Contribution” shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +“submitted” means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as “Not a Contribution.” + +“Contributor” shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +#### 2. Grant of Copyright License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +#### 3. Grant of Patent License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +#### 4. Redistribution + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +* **(a)** You must give any other recipients of the Work or Derivative Works a copy of +this License; and +* **(b)** You must cause any modified files to carry prominent notices stating that You +changed the files; and +* **(c)** You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. + +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +#### 5. Submission of Contributions + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +#### 6. Trademarks + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +#### 7. Disclaimer of Warranty + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an “AS IS” BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +#### 8. Limitation of Liability + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +#### 9. Accepting Warranty or Additional Liability + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +_END OF TERMS AND CONDITIONS_ + +### APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets `[]` replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same “printed page” as the copyright notice for easier identification within +third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/Drivers/gc9a01-module/README.md b/Drivers/gc9a01-module/README.md new file mode 100644 index 000000000..c482da0d9 --- /dev/null +++ b/Drivers/gc9a01-module/README.md @@ -0,0 +1,5 @@ +# GC9A01 display + +A driver for the `GC9A01` round display panel by Galaxycore, over SPI. + +License: [Apache v2.0](LICENSE-Apache-2.0.md) diff --git a/Drivers/gc9a01-module/bindings/galaxycore,gc9a01.yaml b/Drivers/gc9a01-module/bindings/galaxycore,gc9a01.yaml new file mode 100644 index 000000000..27576d1a2 --- /dev/null +++ b/Drivers/gc9a01-module/bindings/galaxycore,gc9a01.yaml @@ -0,0 +1,67 @@ +description: Galaxycore GC9A01 round display panel + +bus: spi + +compatible: "galaxycore,gc9a01" + +properties: + horizontal-resolution: + type: int + required: true + description: Horizontal resolution in pixels + vertical-resolution: + type: int + required: true + description: Vertical resolution in pixels + gap-x: + type: int + default: 0 + description: X offset applied to all draw operations + gap-y: + type: int + default: 0 + description: Y offset applied to all draw operations + swap-xy: + type: boolean + default: false + description: Swap the X and Y axes + mirror-x: + type: boolean + default: false + description: Mirror the X axis + mirror-y: + type: boolean + default: false + description: Mirror the Y axis + invert-color: + type: boolean + default: false + description: Invert the panel's color output + bgr-order: + type: boolean + default: false + description: Use BGR element order instead of RGB + pixel-clock-hz: + type: int + default: 80000000 + description: SPI pixel clock frequency in Hz + transaction-queue-depth: + type: int + default: 10 + description: Size of the internal SPI transaction queue + pin-dc: + type: phandles + required: true + description: Data/Command GPIO pin + pin-reset: + type: phandles + default: GPIO_PIN_SPEC_NONE + description: Reset GPIO pin + reset-active-high: + type: boolean + default: false + description: Whether the reset pin is active high + backlight: + type: phandle + default: "NULL" + description: Optional reference to this display's backlight device diff --git a/Drivers/gc9a01-module/devicetree.yaml b/Drivers/gc9a01-module/devicetree.yaml new file mode 100644 index 000000000..a07d6f334 --- /dev/null +++ b/Drivers/gc9a01-module/devicetree.yaml @@ -0,0 +1,3 @@ +dependencies: + - TactilityKernel +bindings: bindings diff --git a/Drivers/gc9a01-module/include/bindings/gc9a01.h b/Drivers/gc9a01-module/include/bindings/gc9a01.h new file mode 100644 index 000000000..edbdaabc7 --- /dev/null +++ b/Drivers/gc9a01-module/include/bindings/gc9a01.h @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include + +DEFINE_DEVICETREE(gc9a01, struct Gc9a01Config) diff --git a/Drivers/gc9a01-module/include/drivers/gc9a01.h b/Drivers/gc9a01-module/include/drivers/gc9a01.h new file mode 100644 index 000000000..2df79c3f8 --- /dev/null +++ b/Drivers/gc9a01-module/include/drivers/gc9a01.h @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include +#include + +struct Gc9a01Config { + uint16_t horizontal_resolution; + uint16_t vertical_resolution; + int32_t gap_x; + int32_t gap_y; + bool swap_xy; + bool mirror_x; + bool mirror_y; + bool invert_color; + bool bgr_order; + uint32_t pixel_clock_hz; + uint8_t transaction_queue_depth; + struct GpioPinSpec pin_dc; + struct GpioPinSpec pin_reset; + bool reset_active_high; + // Optional reference to this display's backlight device, NULL if none. + struct Device* backlight; +}; + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/gc9a01-module/include/gc9a01_module.h b/Drivers/gc9a01-module/include/gc9a01_module.h new file mode 100644 index 000000000..d452a4d61 --- /dev/null +++ b/Drivers/gc9a01-module/include/gc9a01_module.h @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +extern struct Module gc9a01_module; + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/gc9a01-module/source/gc9a01.cpp b/Drivers/gc9a01-module/source/gc9a01.cpp new file mode 100644 index 000000000..864064044 --- /dev/null +++ b/Drivers/gc9a01-module/source/gc9a01.cpp @@ -0,0 +1,322 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +#include + +constexpr auto* TAG = "GC9A01"; +#define GET_CONFIG(device) (static_cast((device)->config)) + +struct Gc9a01Internal { + esp_lcd_panel_io_handle_t io_handle; + esp_lcd_panel_handle_t panel_handle; + // See st7796-module's identical field for why this exists: draw_bitmap() must block until + // the transfer physically completes (DisplayApi's synchronous contract). + SemaphoreHandle_t draw_done_semaphore; +}; + +static bool IRAM_ATTR on_color_trans_done(esp_lcd_panel_io_handle_t, esp_lcd_panel_io_event_data_t*, void* user_ctx) { + auto* internal = static_cast(user_ctx); + BaseType_t high_task_woken = pdFALSE; + xSemaphoreGiveFromISR(internal->draw_done_semaphore, &high_task_woken); + return high_task_woken == pdTRUE; +} + +static int pin_or_unused(const struct GpioPinSpec& pin) { + return pin.gpio_controller == nullptr ? -1 : static_cast(pin.pin); +} + +// region Driver lifecycle + +static error_t start(Device* device) { + auto* parent = device_get_parent(device); + check(device_get_type(parent) == &SPI_CONTROLLER_TYPE); + + const auto* spi_config = static_cast(parent->config); + const auto* config = GET_CONFIG(device); + + struct GpioPinSpec cs_pin; + if (esp32_spi_get_cs_pin(device, &cs_pin) != ERROR_NONE) { + LOG_E(TAG, "Failed to resolve CS pin"); + return ERROR_RESOURCE; + } + + auto* internal = static_cast(malloc(sizeof(Gc9a01Internal))); + if (internal == nullptr) { + return ERROR_OUT_OF_MEMORY; + } + + internal->draw_done_semaphore = xSemaphoreCreateBinary(); + if (internal->draw_done_semaphore == nullptr) { + free(internal); + return ERROR_OUT_OF_MEMORY; + } + + esp_lcd_panel_io_spi_config_t io_config = { + .cs_gpio_num = pin_or_unused(cs_pin), + .dc_gpio_num = pin_or_unused(config->pin_dc), + .spi_mode = 0, + .pclk_hz = config->pixel_clock_hz, + .trans_queue_depth = config->transaction_queue_depth, + .on_color_trans_done = on_color_trans_done, + .user_ctx = internal, + .lcd_cmd_bits = 8, + .lcd_param_bits = 8, + .cs_ena_pretrans = 0, + .cs_ena_posttrans = 0, + .flags = { + .dc_high_on_cmd = 0, + .dc_low_on_data = 0, + .dc_low_on_param = 0, + .octal_mode = 0, + .quad_mode = 0, + .sio_mode = 0, + .lsb_first = 0, + .cs_high_active = 0, + }, + }; + + esp_err_t ret = esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)spi_config->host, &io_config, &internal->io_handle); + if (ret != ESP_OK) { + LOG_E(TAG, "Failed to create panel IO: %s", esp_err_to_name(ret)); + vSemaphoreDelete(internal->draw_done_semaphore); + free(internal); + return ERROR_RESOURCE; + } + + esp_lcd_panel_dev_config_t panel_config = { + .reset_gpio_num = pin_or_unused(config->pin_reset), + .rgb_ele_order = config->bgr_order ? LCD_RGB_ELEMENT_ORDER_BGR : LCD_RGB_ELEMENT_ORDER_RGB, + .data_endian = LCD_RGB_DATA_ENDIAN_LITTLE, + .bits_per_pixel = 16, + .flags = { .reset_active_high = config->reset_active_high }, + .vendor_config = nullptr, + }; + + ret = esp_lcd_new_panel_gc9a01(internal->io_handle, &panel_config, &internal->panel_handle); + if (ret != ESP_OK) { + LOG_E(TAG, "Failed to create panel: %s", esp_err_to_name(ret)); + esp_lcd_panel_io_del(internal->io_handle); + vSemaphoreDelete(internal->draw_done_semaphore); + free(internal); + return ERROR_RESOURCE; + } + + // Bring-up sequence, order matches the deprecated HAL's Gc9a01Display (proven correct on real + // Waveshare S3 Touch LCD 1.28 hardware). Every failure path below must clean up fully: unlike + // stop_device, this is never retried by the kernel if start_device fails. + bool ok = + esp_lcd_panel_reset(internal->panel_handle) == ESP_OK && + esp_lcd_panel_init(internal->panel_handle) == ESP_OK; + + if (ok) { + ok = (config->gap_x == 0 && config->gap_y == 0) || esp_lcd_panel_set_gap(internal->panel_handle, config->gap_x, config->gap_y) == ESP_OK; + } + ok = ok && (!config->swap_xy || esp_lcd_panel_swap_xy(internal->panel_handle, true) == ESP_OK); + ok = ok && ((!config->mirror_x && !config->mirror_y) || esp_lcd_panel_mirror(internal->panel_handle, config->mirror_x, config->mirror_y) == ESP_OK); + ok = ok && (!config->invert_color || esp_lcd_panel_invert_color(internal->panel_handle, true) == ESP_OK); + ok = ok && esp_lcd_panel_disp_on_off(internal->panel_handle, true) == ESP_OK; + + if (!ok) { + LOG_E(TAG, "Failed to bring up panel"); + esp_lcd_panel_del(internal->panel_handle); + esp_lcd_panel_io_del(internal->io_handle); + vSemaphoreDelete(internal->draw_done_semaphore); + free(internal); + return ERROR_RESOURCE; + } + + device_set_driver_data(device, internal); + return ERROR_NONE; +} + +static error_t stop(Device* device) { + auto* internal = static_cast(device_get_driver_data(device)); + + if (internal->panel_handle != nullptr) { + if (esp_lcd_panel_del(internal->panel_handle) != ESP_OK) { + LOG_E(TAG, "Failed to delete panel"); + return ERROR_RESOURCE; + } + internal->panel_handle = nullptr; + } + + if (internal->io_handle != nullptr) { + if (esp_lcd_panel_io_del(internal->io_handle) != ESP_OK) { + LOG_E(TAG, "Failed to delete panel IO"); + return ERROR_RESOURCE; + } + internal->io_handle = nullptr; + } + + vSemaphoreDelete(internal->draw_done_semaphore); + free(internal); + device_set_driver_data(device, nullptr); + return ERROR_NONE; +} + +// endregion + +// region DisplayApi + +static error_t gc9a01_reset(Device* device) { + auto* internal = static_cast(device_get_driver_data(device)); + return esp_lcd_panel_reset(internal->panel_handle) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE; +} + +static error_t gc9a01_init(Device* device) { + auto* internal = static_cast(device_get_driver_data(device)); + return esp_lcd_panel_init(internal->panel_handle) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE; +} + +static error_t gc9a01_draw_bitmap(Device* device, int32_t x_start, int32_t y_start, int32_t x_end, int32_t y_end, const void* color_data) { + auto* internal = static_cast(device_get_driver_data(device)); + + xSemaphoreTake(internal->draw_done_semaphore, 0); + + if (esp_lcd_panel_draw_bitmap(internal->panel_handle, x_start, y_start, x_end, y_end, color_data) != ESP_OK) { + return ERROR_RESOURCE; + } + + xSemaphoreTake(internal->draw_done_semaphore, portMAX_DELAY); + return ERROR_NONE; +} + +static error_t gc9a01_mirror(Device* device, bool x_axis, bool y_axis) { + auto* internal = static_cast(device_get_driver_data(device)); + return esp_lcd_panel_mirror(internal->panel_handle, x_axis, y_axis) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE; +} + +static error_t gc9a01_swap_xy(Device* device, bool swap_axes) { + auto* internal = static_cast(device_get_driver_data(device)); + return esp_lcd_panel_swap_xy(internal->panel_handle, swap_axes) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE; +} + +static bool gc9a01_get_swap_xy(Device* device) { + return GET_CONFIG(device)->swap_xy; +} + +static bool gc9a01_get_mirror_x(Device* device) { + return GET_CONFIG(device)->mirror_x; +} + +static bool gc9a01_get_mirror_y(Device* device) { + return GET_CONFIG(device)->mirror_y; +} + +static error_t gc9a01_set_gap(Device* device, int32_t x_gap, int32_t y_gap) { + auto* internal = static_cast(device_get_driver_data(device)); + return esp_lcd_panel_set_gap(internal->panel_handle, x_gap, y_gap) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE; +} + +static int32_t gc9a01_get_gap_x(Device* device) { + return GET_CONFIG(device)->gap_x; +} + +static int32_t gc9a01_get_gap_y(Device* device) { + return GET_CONFIG(device)->gap_y; +} + +static error_t gc9a01_invert_color(Device* device, bool invert_color_data) { + auto* internal = static_cast(device_get_driver_data(device)); + return esp_lcd_panel_invert_color(internal->panel_handle, invert_color_data) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE; +} + +static error_t gc9a01_disp_on_off(Device* device, bool on_off) { + auto* internal = static_cast(device_get_driver_data(device)); + return esp_lcd_panel_disp_on_off(internal->panel_handle, on_off) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE; +} + +static error_t gc9a01_disp_sleep(Device* device, bool sleep) { + auto* internal = static_cast(device_get_driver_data(device)); + return esp_lcd_panel_disp_sleep(internal->panel_handle, sleep) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE; +} + +// The deprecated HAL's Gc9a01Display always set swap_bytes=true on its lvgl_port config +// regardless of rgb_ele_order, so the byte-swapped format applies unconditionally here too - +// bgr_order only affects the panel's hardware element order (applied in start(), above). +static enum DisplayColorFormat gc9a01_get_color_format(Device*) { + return DISPLAY_COLOR_FORMAT_RGB565_SWAPPED; +} + +static uint16_t gc9a01_get_resolution_x(Device* device) { + return GET_CONFIG(device)->horizontal_resolution; +} + +static uint16_t gc9a01_get_resolution_y(Device* device) { + return GET_CONFIG(device)->vertical_resolution; +} + +static void gc9a01_get_frame_buffer(Device*, uint8_t, void** out_buffer) { + *out_buffer = nullptr; +} + +static uint8_t gc9a01_get_frame_buffer_count(Device*) { + return 0; +} + +static error_t gc9a01_get_backlight(Device* device, Device** backlight) { + auto* configured_backlight = GET_CONFIG(device)->backlight; + if (configured_backlight == nullptr) { + return ERROR_NOT_SUPPORTED; + } + *backlight = configured_backlight; + return ERROR_NONE; +} + +// endregion + +static const DisplayApi gc9a01_display_api = { + .capabilities = DISPLAY_CAPABILITY_CAP_MIRROR | DISPLAY_CAPABILITY_CAP_SWAP_XY | + DISPLAY_CAPABILITY_CAP_SET_GAP | DISPLAY_CAPABILITY_INVERT_COLOR | DISPLAY_CAPABILITY_ON_OFF | + DISPLAY_CAPABILITY_SLEEP | DISPLAY_CAPABILITY_BACKLIGHT, + .reset = gc9a01_reset, + .init = gc9a01_init, + .draw_bitmap = gc9a01_draw_bitmap, + .mirror = gc9a01_mirror, + .swap_xy = gc9a01_swap_xy, + .get_swap_xy = gc9a01_get_swap_xy, + .get_mirror_x = gc9a01_get_mirror_x, + .get_mirror_y = gc9a01_get_mirror_y, + .set_gap = gc9a01_set_gap, + .get_gap_x = gc9a01_get_gap_x, + .get_gap_y = gc9a01_get_gap_y, + .invert_color = gc9a01_invert_color, + .disp_on_off = gc9a01_disp_on_off, + .disp_sleep = gc9a01_disp_sleep, + .get_color_format = gc9a01_get_color_format, + .get_resolution_x = gc9a01_get_resolution_x, + .get_resolution_y = gc9a01_get_resolution_y, + .get_frame_buffer = gc9a01_get_frame_buffer, + .get_frame_buffer_count = gc9a01_get_frame_buffer_count, + .get_backlight = gc9a01_get_backlight, + .has_capability = nullptr, +}; + +Driver gc9a01_driver = { + .name = "gc9a01", + .compatible = (const char*[]) { "galaxycore,gc9a01", nullptr }, + .start_device = start, + .stop_device = stop, + .api = &gc9a01_display_api, + .device_type = &DISPLAY_TYPE, + .owner = &gc9a01_module, + .internal = nullptr +}; diff --git a/Drivers/gc9a01-module/source/module.cpp b/Drivers/gc9a01-module/source/module.cpp new file mode 100644 index 000000000..9c03f96f6 --- /dev/null +++ b/Drivers/gc9a01-module/source/module.cpp @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include + +extern "C" { + +extern Driver gc9a01_driver; + +static error_t start() { + /* We crash when construct fails, because if a single driver fails to construct, + * there is no guarantee that the previously constructed drivers can be destroyed */ + check(driver_construct_add(&gc9a01_driver) == ERROR_NONE); + return ERROR_NONE; +} + +static error_t stop() { + /* We crash when destruct fails, because if a single driver fails to destruct, + * there is no guarantee that the previously destroyed drivers can be recovered */ + check(driver_remove_destruct(&gc9a01_driver) == ERROR_NONE); + return ERROR_NONE; +} + +Module gc9a01_module = { + .name = "gc9a01", + .start = start, + .stop = stop, + .symbols = nullptr, + .internal = nullptr +}; + +} // extern "C" diff --git a/Drivers/ili9881c-module/CMakeLists.txt b/Drivers/ili9881c-module/CMakeLists.txt new file mode 100644 index 000000000..7c06ed96f --- /dev/null +++ b/Drivers/ili9881c-module/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.20) + +include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake") + +file(GLOB_RECURSE SOURCE_FILES "source/*.c*") + +tactility_add_module(ili9881c-module + SRCS ${SOURCE_FILES} + INCLUDE_DIRS include/ + REQUIRES TactilityKernel platform-esp32 esp_lcd_ili9881c esp_lcd driver esp_hw_support +) diff --git a/Drivers/ili9881c-module/LICENSE-Apache-2.0.md b/Drivers/ili9881c-module/LICENSE-Apache-2.0.md new file mode 100644 index 000000000..f5f4b8b5e --- /dev/null +++ b/Drivers/ili9881c-module/LICENSE-Apache-2.0.md @@ -0,0 +1,195 @@ +Apache License +============== + +_Version 2.0, January 2004_ +_<>_ + +### Terms and Conditions for use, reproduction, and distribution + +#### 1. Definitions + +“License” shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +“Licensor” shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +“Legal Entity” shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, “control” means **(i)** the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the +outstanding shares, or **(iii)** beneficial ownership of such entity. + +“You” (or “Your”) shall mean an individual or Legal Entity exercising +permissions granted by this License. + +“Source” form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +“Object” form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +“Work” shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +“Derivative Works” shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +“Contribution” shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +“submitted” means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as “Not a Contribution.” + +“Contributor” shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +#### 2. Grant of Copyright License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +#### 3. Grant of Patent License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +#### 4. Redistribution + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +* **(a)** You must give any other recipients of the Work or Derivative Works a copy of +this License; and +* **(b)** You must cause any modified files to carry prominent notices stating that You +changed the files; and +* **(c)** You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. + +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +#### 5. Submission of Contributions + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +#### 6. Trademarks + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +#### 7. Disclaimer of Warranty + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an “AS IS” BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +#### 8. Limitation of Liability + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +#### 9. Accepting Warranty or Additional Liability + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +_END OF TERMS AND CONDITIONS_ + +### APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets `[]` replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same “printed page” as the copyright notice for easier identification within +third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/Drivers/ili9881c-module/bindings/ilitek,ili9881c.yaml b/Drivers/ili9881c-module/bindings/ilitek,ili9881c.yaml new file mode 100644 index 000000000..794032dbb --- /dev/null +++ b/Drivers/ili9881c-module/bindings/ilitek,ili9881c.yaml @@ -0,0 +1,128 @@ +description: > + Ilitek ILI9881C MIPI-DSI display panel, driven over ESP-IDF's esp_lcd_ili9881c component + (DPI/DBI interface, ESP32-P4 and other SOC_MIPI_DSI_SUPPORTED targets only). Owns the MIPI DSI + PHY LDO channel and DSI bus directly, so unlike SPI/RGB panels it has no parent bus controller + device. + +compatible: "ilitek,ili9881c" + +properties: + horizontal-resolution: + type: int + required: true + description: Horizontal resolution in pixels + vertical-resolution: + type: int + required: true + description: Vertical resolution in pixels + bits-per-pixel: + type: int + default: 16 + description: Color depth in bits per pixel (16, 18 or 24) + bgr-order: + type: boolean + default: false + description: Use BGR element order instead of RGB + invert-color: + type: boolean + default: false + description: Invert the panel's color output + mirror-x: + type: boolean + default: false + description: Mirror the X axis + mirror-y: + type: boolean + default: false + description: Mirror the Y axis + pin-reset: + type: phandles + default: GPIO_PIN_SPEC_NONE + description: Reset GPIO pin. Falls back to a software reset (sent over the DBI command + interface) if not set. + reset-active-high: + type: boolean + default: false + description: Whether the reset pin is active high + ldo-channel: + type: int + required: true + description: LDO channel index powering the MIPI DSI PHY (chan_id in esp_ldo_channel_config_t) + ldo-voltage-mv: + type: int + required: true + description: Voltage to supply to the MIPI DSI PHY LDO channel, in mV + dsi-bus-id: + type: int + default: 0 + description: Which DSI controller to use, index from 0 + num-data-lanes: + type: int + default: 2 + description: Number of MIPI DSI data lanes. Also used as the vendor config's lane count. 0 + falls back to the maximum available. + lane-bit-rate-mbps: + type: int + required: true + description: MIPI DSI lane bit rate, in Mbps + dpi-clock-freq-mhz: + type: int + required: true + description: MIPI DPI clock frequency, in MHz + hsync-pulse-width: + type: int + required: true + description: Horizontal sync width, in PCLK periods + hsync-back-porch: + type: int + required: true + description: Number of PCLK periods between hsync and the start of line active data + hsync-front-porch: + type: int + required: true + description: Number of PCLK periods between the end of active data and the next hsync + vsync-pulse-width: + type: int + required: true + description: Vertical sync width, in lines + vsync-back-porch: + type: int + required: true + description: Number of invalid lines between vsync and the start of the frame + vsync-front-porch: + type: int + required: true + description: Number of invalid lines between the end of the frame and the next vsync + num-fbs: + type: int + default: 1 + description: Number of screen-sized frame buffers to allocate (0 or 1 = single-buffered) + use-dma2d: + type: boolean + default: true + description: Use DMA2D to copy user buffers into the frame buffer when necessary (only + meaningful on SOC_DMA2D_SUPPORTED targets - must be false otherwise) + disable-lp: + type: boolean + default: false + description: Disable MIPI DSI low-power mode + allow-tearing: + type: boolean + default: false + description: By default, when draw_bitmap's color_data is one of the panel's own frame + buffers (i.e. LVGL is bound directly onto them), draw_bitmap waits for a full scan-out to + complete before returning so the caller can't start overwriting a buffer still being + displayed. Set this to trade away that tear-free guarantee for lower latency (e.g. when + other tasks occasionally block timing for long enough that waiting causes visible stalls). + init-sequence: + type: array + element-type: uint8_t + description: > + Custom vendor bring-up sequence, flattened into bytes as a run of + [cmd, data-length, delay-ms, data-length bytes of data...] entries, e.g. + `init-sequence = [0x11 0 120 0x29 0 20];`. + Omit to use the ILI9881C component's own built-in default sequence. + backlight: + type: phandle + default: "NULL" + description: Optional reference to this display's backlight device diff --git a/Drivers/ili9881c-module/devicetree.yaml b/Drivers/ili9881c-module/devicetree.yaml new file mode 100644 index 000000000..a07d6f334 --- /dev/null +++ b/Drivers/ili9881c-module/devicetree.yaml @@ -0,0 +1,3 @@ +dependencies: + - TactilityKernel +bindings: bindings diff --git a/Drivers/ili9881c-module/include/bindings/ili9881c.h b/Drivers/ili9881c-module/include/bindings/ili9881c.h new file mode 100644 index 000000000..baf36bee1 --- /dev/null +++ b/Drivers/ili9881c-module/include/bindings/ili9881c.h @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include + +// The devicetree compiler derives the expected config typedef name from the compatible +// string's suffix (e.g. "ilitek,ili9881c" -> ili9881c_config_dt), not from the node name or +// driver name, so the tag here must match that exactly. +DEFINE_DEVICETREE(ili9881c, struct Ili9881cConfig) diff --git a/Drivers/ili9881c-module/include/drivers/ili9881c.h b/Drivers/ili9881c-module/include/drivers/ili9881c.h new file mode 100644 index 000000000..ca01a2cc3 --- /dev/null +++ b/Drivers/ili9881c-module/include/drivers/ili9881c.h @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include +#include + +struct Ili9881cConfig { + uint16_t horizontal_resolution; + uint16_t vertical_resolution; + uint8_t bits_per_pixel; + bool bgr_order; + bool invert_color; + bool mirror_x; + bool mirror_y; + + // Reset pin for the panel. GPIO_PIN_SPEC_NONE falls back to a software reset sent over the + // DBI command interface (see esp_lcd_ili9881c's panel_ili9881c_reset()). + struct GpioPinSpec pin_reset; + bool reset_active_high; + + // LDO channel powering the MIPI DSI PHY - the PHY has no power of its own until this is + // acquired, so it must happen before the DSI bus is created. Both fields are int32_t (not + // uint32_t) to exactly match esp_ldo_channel_config_t's plain `int` fields - assigning a + // uint32_t into that struct's brace-init would be a narrowing conversion (-Werror=narrowing). + int32_t ldo_channel; + int32_t ldo_voltage_mv; + + uint8_t dsi_bus_id; + // Number of MIPI DSI data lanes. Also passed straight through as the vendor config's + // mipi_config.lane_num (unlike jd9165, ili9881c_vendor_config_t tracks this separately from + // the DSI bus config). 0 falls back to the maximum available. + uint8_t num_data_lanes; + uint32_t lane_bit_rate_mbps; + + uint32_t dpi_clock_freq_mhz; + uint32_t hsync_pulse_width; + uint32_t hsync_back_porch; + uint32_t hsync_front_porch; + uint32_t vsync_pulse_width; + uint32_t vsync_back_porch; + uint32_t vsync_front_porch; + // Number of screen-sized frame buffers the driver allocates (0 or 1 = single-buffered). + uint8_t num_fbs; + bool use_dma2d; + bool disable_lp; + + // See the 'allow-tearing' binding property. + bool allow_tearing; + + // Custom vendor init sequence, flattened as bytes: a run of + // [cmd, data_len, delay_ms, data_len bytes of data...] entries. NULL/0 falls back to the + // ILI9881C component's own built-in default sequence (see vendor_specific_init_default in + // esp_lcd_ili9881c.c) - not guaranteed to match any particular panel's actual bring-up + // requirements. + const uint8_t* init_sequence; + uint32_t init_sequence_length; + + // Optional reference to this display's backlight device, NULL if none. + struct Device* backlight; +}; + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/ili9881c-module/include/ili9881c_module.h b/Drivers/ili9881c-module/include/ili9881c_module.h new file mode 100644 index 000000000..aa7853146 --- /dev/null +++ b/Drivers/ili9881c-module/include/ili9881c_module.h @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +extern struct Module ili9881c_module; + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/ili9881c-module/source/ili9881c.cpp b/Drivers/ili9881c-module/source/ili9881c.cpp new file mode 100644 index 000000000..660249e1b --- /dev/null +++ b/Drivers/ili9881c-module/source/ili9881c.cpp @@ -0,0 +1,526 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#if SOC_MIPI_DSI_SUPPORTED + +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +constexpr auto* TAG = "ILI9881C"; +#define GET_CONFIG(device) (static_cast((device)->config)) + +// Generic lvgl-module display glue (Modules/lvgl-module/source/lvgl_display.c) only ever asks +// for frame buffer index 0 and 1, so caching more than that would be dead weight. +constexpr size_t MAX_CACHED_FRAME_BUFFERS = 2; + +struct Ili9881cInternal { + esp_ldo_channel_handle_t ldo_handle; + esp_lcd_dsi_bus_handle_t dsi_bus_handle; + esp_lcd_panel_io_handle_t io_handle; + esp_lcd_panel_handle_t panel_handle; + void* frame_buffers[MAX_CACHED_FRAME_BUFFERS]; + uint8_t frame_buffer_count; + // Size of each buffer in frame_buffers, in bytes - used to range-check whether a given + // draw_bitmap() color_data pointer is actually one of them (see draw_bitmap() below). + size_t frame_buffer_size_bytes; + // Signaled by on_refresh_done once per real scan-out of a whole frame. Only waited on in + // draw_bitmap() when color_data is one of frame_buffers and avoid_tearing is set - see the + // comment there for why. + SemaphoreHandle_t frame_complete_semaphore; + // Heap-allocated only when the devicetree supplies a custom init_sequence (see + // parse_init_sequence()) - nullptr otherwise, since the vendor's built-in default sequence + // needs no parsing. Its .data pointers alias directly into the devicetree's static const + // byte buffer, so only this struct array itself needs freeing in stop(). + ili9881c_lcd_init_cmd_t* parsed_init_cmds; +}; + +// esp_lcd_dpi_panel's draw_bitmap() has a zero-copy path when color_data is one of the panel's +// own frame buffers (as returned by esp_lcd_dpi_panel_get_frame_buffer()): it just repoints which +// buffer is scanned out and returns almost instantly - well before the DSI peripheral has +// actually finished scanning out the *previous* buffer, let alone started on this one. Callers in +// full/direct LVGL render mode render straight into these real frame buffers, so if draw_bitmap() +// returned that quickly, LVGL would be free to start overwriting the *other* buffer - which may +// still be mid-scanout - producing visible tearing/flashing. on_refresh_done fires once per actual +// whole-frame scan-out completion (continuously, at the panel's refresh rate, independent of +// draw_bitmap calls), so waiting for the next occurrence after each draw_bitmap() genuinely blocks +// until it's safe to start writing into the frame buffers again - unless avoid_tearing is false, +// in which case the caller has opted out of this wait (see the binding property). +static bool on_refresh_done(esp_lcd_panel_handle_t, esp_lcd_dpi_panel_event_data_t*, void* user_ctx) { + auto* internal = static_cast(user_ctx); + BaseType_t high_task_woken = pdFALSE; + xSemaphoreGiveFromISR(internal->frame_complete_semaphore, &high_task_woken); + return high_task_woken == pdTRUE; +} + +static int pin_or_unused(const GpioPinSpec& pin) { + return pin.gpio_controller == nullptr ? -1 : static_cast(pin.pin); +} + +static lcd_color_format_t color_format_from_bits_per_pixel(uint8_t bits_per_pixel) { + switch (bits_per_pixel) { + case 18: return LCD_COLOR_FMT_RGB666; + case 24: return LCD_COLOR_FMT_RGB888; + default: return LCD_COLOR_FMT_RGB565; + } +} + +// Unpacks the devicetree's flat [cmd, data_len, delay_ms, data_len bytes...] encoding (produced +// by the devicetree compiler's "array" property type - see init-sequence in +// bindings/ilitek,ili9881c.yaml) into a heap-allocated ili9881c_lcd_init_cmd_t array. Each entry's +// .data points directly into `bytes`, which is the devicetree's static const buffer and outlives +// the device, so no per-entry copy is needed. +static bool parse_init_sequence(const uint8_t* bytes, uint32_t length, ili9881c_lcd_init_cmd_t** out_cmds, uint16_t* out_count) { + uint32_t count = 0; + for (uint32_t offset = 0; offset < length; count++) { + if (offset + 3 > length) { + LOG_E(TAG, "init-sequence truncated: entry header runs past the end of the array"); + return false; + } + offset += 3 + bytes[offset + 1]; + if (offset > length) { + LOG_E(TAG, "init-sequence truncated: entry data runs past the end of the array"); + return false; + } + } + + auto* cmds = static_cast(malloc(count * sizeof(ili9881c_lcd_init_cmd_t))); + if (cmds == nullptr) { + return false; + } + + uint32_t offset = 0; + for (uint32_t i = 0; i < count; i++) { + uint8_t data_len = bytes[offset + 1]; + cmds[i] = { + .cmd = bytes[offset], + .data = data_len > 0 ? &bytes[offset + 3] : nullptr, + .data_bytes = data_len, + .delay_ms = bytes[offset + 2], + }; + offset += 3 + data_len; + } + + *out_cmds = cmds; + *out_count = (uint16_t)count; + return true; +} + +// region Driver lifecycle + +static error_t start(Device* device) { + const auto* config = GET_CONFIG(device); + + auto* internal = static_cast(malloc(sizeof(Ili9881cInternal))); + if (internal == nullptr) { + return ERROR_OUT_OF_MEMORY; + } + internal->parsed_init_cmds = nullptr; + + const ili9881c_lcd_init_cmd_t* init_cmds = nullptr; + uint16_t init_cmds_size = 0; + if (config->init_sequence != nullptr && config->init_sequence_length > 0) { + if (!parse_init_sequence(config->init_sequence, config->init_sequence_length, &internal->parsed_init_cmds, &init_cmds_size)) { + LOG_E(TAG, "Failed to parse init-sequence property"); + free(internal); + return ERROR_INVALID_ARGUMENT; + } + init_cmds = internal->parsed_init_cmds; + } + + // The MIPI DSI PHY has no power of its own until this LDO channel is enabled - must happen + // before the DSI bus is created. + esp_ldo_channel_config_t ldo_config = { + .chan_id = config->ldo_channel, + .voltage_mv = config->ldo_voltage_mv, + .flags = {}, + }; + if (esp_ldo_acquire_channel(&ldo_config, &internal->ldo_handle) != ESP_OK) { + LOG_E(TAG, "Failed to acquire LDO channel for MIPI DSI PHY"); + free(internal->parsed_init_cmds); + free(internal); + return ERROR_RESOURCE; + } + + const esp_lcd_dsi_bus_config_t bus_config = { + .bus_id = config->dsi_bus_id, + .num_data_lanes = config->num_data_lanes, + .phy_clk_src = MIPI_DSI_PHY_CLK_SRC_DEFAULT, + .lane_bit_rate_mbps = config->lane_bit_rate_mbps, + }; + if (esp_lcd_new_dsi_bus(&bus_config, &internal->dsi_bus_handle) != ESP_OK) { + LOG_E(TAG, "Failed to create MIPI DSI bus"); + esp_ldo_release_channel(internal->ldo_handle); + free(internal->parsed_init_cmds); + free(internal); + return ERROR_RESOURCE; + } + + const 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(internal->dsi_bus_handle, &dbi_config, &internal->io_handle) != ESP_OK) { + LOG_E(TAG, "Failed to create panel IO"); + esp_lcd_del_dsi_bus(internal->dsi_bus_handle); + esp_ldo_release_channel(internal->ldo_handle); + free(internal->parsed_init_cmds); + free(internal); + return ERROR_RESOURCE; + } + + const lcd_color_format_t color_format = color_format_from_bits_per_pixel(config->bits_per_pixel); + const esp_lcd_dpi_panel_config_t dpi_config = { + .virtual_channel = 0, + .dpi_clk_src = MIPI_DSI_DPI_CLK_SRC_DEFAULT, + .dpi_clock_freq_mhz = config->dpi_clock_freq_mhz, + .pixel_format = (lcd_color_rgb_pixel_format_t)0, // deprecated field - in/out_color_format below take precedence + .in_color_format = color_format, + .out_color_format = color_format, + .num_fbs = config->num_fbs, + .video_timing = { + .h_size = config->horizontal_resolution, + .v_size = config->vertical_resolution, + .hsync_pulse_width = config->hsync_pulse_width, + .hsync_back_porch = config->hsync_back_porch, + .hsync_front_porch = config->hsync_front_porch, + .vsync_pulse_width = config->vsync_pulse_width, + .vsync_back_porch = config->vsync_back_porch, + .vsync_front_porch = config->vsync_front_porch, + }, + .flags = { + .use_dma2d = config->use_dma2d, + .disable_lp = config->disable_lp, + }, + }; + + ili9881c_vendor_config_t vendor_config = { + .init_cmds = init_cmds, + .init_cmds_size = init_cmds_size, + .mipi_config = { + .dsi_bus = internal->dsi_bus_handle, + .dpi_config = &dpi_config, + .lane_num = config->num_data_lanes, + }, + }; + + const esp_lcd_panel_dev_config_t panel_config = { + .reset_gpio_num = pin_or_unused(config->pin_reset), + .rgb_ele_order = config->bgr_order ? LCD_RGB_ELEMENT_ORDER_BGR : LCD_RGB_ELEMENT_ORDER_RGB, + .data_endian = LCD_RGB_DATA_ENDIAN_LITTLE, + .bits_per_pixel = config->bits_per_pixel, + .flags = { .reset_active_high = config->reset_active_high }, + .vendor_config = &vendor_config, + }; + + if (esp_lcd_new_panel_ili9881c(internal->io_handle, &panel_config, &internal->panel_handle) != ESP_OK) { + LOG_E(TAG, "Failed to create panel"); + esp_lcd_panel_io_del(internal->io_handle); + esp_lcd_del_dsi_bus(internal->dsi_bus_handle); + esp_ldo_release_channel(internal->ldo_handle); + free(internal->parsed_init_cmds); + free(internal); + return ERROR_RESOURCE; + } + + // Bring-up sequence: reset() pulses (or software-resets) the panel and init() pushes + // init_cmds over the DBI command interface before bringing up the underlying DPI peripheral. + // swap_xy/set_gap are intentionally not called: the ILI9881C driver doesn't override them and + // the underlying raw DPI panel doesn't implement them either, so both would just fail with + // ESP_ERR_NOT_SUPPORTED (see DisplayApi below). Every failure path here must clean up fully: + // unlike stop_device, this is never retried by the kernel if start_device fails (see + // device_start() in TactilityKernel), so a partial failure here would leak. + bool ok = + esp_lcd_panel_reset(internal->panel_handle) == ESP_OK && + esp_lcd_panel_init(internal->panel_handle) == ESP_OK && + esp_lcd_panel_invert_color(internal->panel_handle, config->invert_color) == ESP_OK && + esp_lcd_panel_mirror(internal->panel_handle, config->mirror_x, config->mirror_y) == ESP_OK && + esp_lcd_panel_disp_on_off(internal->panel_handle, true) == ESP_OK; + + if (!ok) { + LOG_E(TAG, "Failed to bring up panel"); + esp_lcd_panel_del(internal->panel_handle); + esp_lcd_panel_io_del(internal->io_handle); + esp_lcd_del_dsi_bus(internal->dsi_bus_handle); + esp_ldo_release_channel(internal->ldo_handle); + free(internal->parsed_init_cmds); + free(internal); + return ERROR_RESOURCE; + } + + internal->frame_buffer_count = 0; + internal->frame_buffer_size_bytes = (size_t)config->horizontal_resolution * config->vertical_resolution * + ((config->bits_per_pixel + 7) / 8); + if (config->num_fbs > 0) { + // esp_lcd_dpi_panel_get_frame_buffer() is variadic: the number of out-pointer arguments + // passed must match fb_num exactly, so this can't be a loop. + size_t fb_num = config->num_fbs < MAX_CACHED_FRAME_BUFFERS ? config->num_fbs : MAX_CACHED_FRAME_BUFFERS; + esp_err_t ret; + switch (fb_num) { + case 1: + ret = esp_lcd_dpi_panel_get_frame_buffer(internal->panel_handle, 1, &internal->frame_buffers[0]); + break; + case 2: + ret = esp_lcd_dpi_panel_get_frame_buffer(internal->panel_handle, 2, &internal->frame_buffers[0], &internal->frame_buffers[1]); + break; + default: + ret = ESP_OK; + break; + } + + if (ret != ESP_OK) { + LOG_E(TAG, "Failed to get frame buffer(s): %s", esp_err_to_name(ret)); + esp_lcd_panel_del(internal->panel_handle); + esp_lcd_panel_io_del(internal->io_handle); + esp_lcd_del_dsi_bus(internal->dsi_bus_handle); + esp_ldo_release_channel(internal->ldo_handle); + free(internal->parsed_init_cmds); + free(internal); + return ERROR_RESOURCE; + } + internal->frame_buffer_count = (uint8_t)fb_num; + } + + internal->frame_complete_semaphore = xSemaphoreCreateBinary(); + if (internal->frame_complete_semaphore == nullptr) { + esp_lcd_panel_del(internal->panel_handle); + esp_lcd_panel_io_del(internal->io_handle); + esp_lcd_del_dsi_bus(internal->dsi_bus_handle); + esp_ldo_release_channel(internal->ldo_handle); + free(internal->parsed_init_cmds); + free(internal); + return ERROR_OUT_OF_MEMORY; + } + + esp_lcd_dpi_panel_event_callbacks_t callbacks = {}; + callbacks.on_refresh_done = on_refresh_done; + if (esp_lcd_dpi_panel_register_event_callbacks(internal->panel_handle, &callbacks, internal) != ESP_OK) { + LOG_E(TAG, "Failed to register panel event callbacks"); + vSemaphoreDelete(internal->frame_complete_semaphore); + esp_lcd_panel_del(internal->panel_handle); + esp_lcd_panel_io_del(internal->io_handle); + esp_lcd_del_dsi_bus(internal->dsi_bus_handle); + esp_ldo_release_channel(internal->ldo_handle); + free(internal->parsed_init_cmds); + free(internal); + return ERROR_RESOURCE; + } + + device_set_driver_data(device, internal); + return ERROR_NONE; +} + +static error_t stop(Device* device) { + auto* internal = static_cast(device_get_driver_data(device)); + + if (internal->panel_handle != nullptr) { + if (esp_lcd_panel_del(internal->panel_handle) != ESP_OK) { + LOG_E(TAG, "Failed to delete panel"); + return ERROR_RESOURCE; + } + internal->panel_handle = nullptr; + } + + if (internal->io_handle != nullptr) { + if (esp_lcd_panel_io_del(internal->io_handle) != ESP_OK) { + LOG_E(TAG, "Failed to delete panel IO"); + return ERROR_RESOURCE; + } + internal->io_handle = nullptr; + } + + if (internal->dsi_bus_handle != nullptr) { + if (esp_lcd_del_dsi_bus(internal->dsi_bus_handle) != ESP_OK) { + LOG_E(TAG, "Failed to delete DSI bus"); + return ERROR_RESOURCE; + } + internal->dsi_bus_handle = nullptr; + } + + if (internal->ldo_handle != nullptr) { + if (esp_ldo_release_channel(internal->ldo_handle) != ESP_OK) { + LOG_E(TAG, "Failed to release LDO channel"); + return ERROR_RESOURCE; + } + internal->ldo_handle = nullptr; + } + + vSemaphoreDelete(internal->frame_complete_semaphore); + free(internal->parsed_init_cmds); + free(internal); + device_set_driver_data(device, nullptr); + return ERROR_NONE; +} + +// endregion + +// region DisplayApi + +static error_t ili9881c_reset(Device* device) { + auto* internal = static_cast(device_get_driver_data(device)); + return esp_lcd_panel_reset(internal->panel_handle) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE; +} + +static error_t ili9881c_init(Device* device) { + auto* internal = static_cast(device_get_driver_data(device)); + return esp_lcd_panel_init(internal->panel_handle) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE; +} + +// Only block for scan-out completion when color_data is actually one of the panel's own frame +// buffers (see on_refresh_done's comment above for why that matters) and allow_tearing is not +// set - i.e. this specific call is a zero-copy flip, not a plain CPU copy into the panel's buffer +// from a caller-owned one (e.g. LVGL bound in owned-buffer mode), which has no reuse race to +// guard against and shouldn't pay the up-to-one-frame latency cost for every partial update. +static bool ili9881c_color_data_is_frame_buffer(const Ili9881cInternal* internal, const void* color_data) { + const auto* ptr = static_cast(color_data); + for (uint8_t i = 0; i < internal->frame_buffer_count; i++) { + const auto* base = static_cast(internal->frame_buffers[i]); + if (ptr >= base && ptr < base + internal->frame_buffer_size_bytes) { + return true; + } + } + return false; +} + +static error_t ili9881c_draw_bitmap(Device* device, int32_t x_start, int32_t y_start, int32_t x_end, int32_t y_end, const void* color_data) { + auto* internal = static_cast(device_get_driver_data(device)); + + bool wait_for_scanout = !GET_CONFIG(device)->allow_tearing && ili9881c_color_data_is_frame_buffer(internal, color_data); + if (wait_for_scanout) { + xSemaphoreTake(internal->frame_complete_semaphore, 0); // clear any already-pending signal + } + + if (esp_lcd_panel_draw_bitmap(internal->panel_handle, x_start, y_start, x_end, y_end, color_data) != ESP_OK) { + return ERROR_RESOURCE; + } + + if (wait_for_scanout) { + xSemaphoreTake(internal->frame_complete_semaphore, portMAX_DELAY); + } + + return ERROR_NONE; +} + +// Mirror is always implemented by LCD command (MADCTL), unconditionally, by esp_lcd_ili9881c - +// see panel_ili9881c_mirror() in esp_lcd_ili9881c.c. Unlike esp_lcd_rgb_panel's software +// rotate_mask trick, this isn't tied to draw_bitmap's copy path, so it stays available even when +// LVGL is bound directly onto the panel's own frame buffers. +static error_t ili9881c_mirror(Device* device, bool x_axis, bool y_axis) { + auto* internal = static_cast(device_get_driver_data(device)); + return esp_lcd_panel_mirror(internal->panel_handle, x_axis, y_axis) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE; +} + +static bool ili9881c_get_mirror_x(Device* device) { + return GET_CONFIG(device)->mirror_x; +} + +static bool ili9881c_get_mirror_y(Device* device) { + return GET_CONFIG(device)->mirror_y; +} + +// swap_xy/set_gap are not exposed: esp_lcd_ili9881c doesn't override them and the underlying raw +// MIPI DPI panel doesn't implement them either (esp_lcd_panel_dpi.c never assigns those function +// pointers), so esp_lcd_panel_swap_xy()/set_gap() would just return ESP_ERR_NOT_SUPPORTED. + +static error_t ili9881c_invert_color(Device* device, bool invert_color_data) { + auto* internal = static_cast(device_get_driver_data(device)); + return esp_lcd_panel_invert_color(internal->panel_handle, invert_color_data) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE; +} + +static error_t ili9881c_disp_on_off(Device* device, bool on_off) { + auto* internal = static_cast(device_get_driver_data(device)); + return esp_lcd_panel_disp_on_off(internal->panel_handle, on_off) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE; +} + +// disp_sleep is not exposed: esp_lcd_ili9881c doesn't override it either (only +// del/init/reset/mirror/invert_color/disp_on_off - see esp_lcd_ili9881c.c). + +// bgr_order only selects the panel controller's rgb_ele_order (applied in start(), above) so the +// R/B swap happens on-chip. LVGL always fills the same-layout buffer either way - there's no +// separate "BGR" memory layout to produce, unlike the SPI byte-order swap some other panels need. +static enum DisplayColorFormat ili9881c_get_color_format(Device* device) { + return GET_CONFIG(device)->bits_per_pixel == 24 ? DISPLAY_COLOR_FORMAT_RGB888 : DISPLAY_COLOR_FORMAT_RGB565; +} + +static uint16_t ili9881c_get_resolution_x(Device* device) { + return GET_CONFIG(device)->horizontal_resolution; +} + +static uint16_t ili9881c_get_resolution_y(Device* device) { + return GET_CONFIG(device)->vertical_resolution; +} + +static void ili9881c_get_frame_buffer(Device* device, uint8_t index, void** out_buffer) { + auto* internal = static_cast(device_get_driver_data(device)); + *out_buffer = index < internal->frame_buffer_count ? internal->frame_buffers[index] : nullptr; +} + +static uint8_t ili9881c_get_frame_buffer_count(Device* device) { + auto* internal = static_cast(device_get_driver_data(device)); + return internal->frame_buffer_count; +} + +static error_t ili9881c_get_backlight(Device* device, Device** backlight) { + auto* configured_backlight = GET_CONFIG(device)->backlight; + if (configured_backlight == nullptr) { + return ERROR_NOT_SUPPORTED; + } + *backlight = configured_backlight; + return ERROR_NONE; +} + +// endregion + +static const DisplayApi ili9881c_display_api = { + .capabilities = DISPLAY_CAPABILITY_CAP_MIRROR | DISPLAY_CAPABILITY_INVERT_COLOR | + DISPLAY_CAPABILITY_ON_OFF | DISPLAY_CAPABILITY_BACKLIGHT, + .reset = ili9881c_reset, + .init = ili9881c_init, + .draw_bitmap = ili9881c_draw_bitmap, + .mirror = ili9881c_mirror, + .swap_xy = nullptr, + .get_swap_xy = nullptr, + .get_mirror_x = ili9881c_get_mirror_x, + .get_mirror_y = ili9881c_get_mirror_y, + .set_gap = nullptr, + .invert_color = ili9881c_invert_color, + .disp_on_off = ili9881c_disp_on_off, + .disp_sleep = nullptr, + .get_color_format = ili9881c_get_color_format, + .get_resolution_x = ili9881c_get_resolution_x, + .get_resolution_y = ili9881c_get_resolution_y, + .get_frame_buffer = ili9881c_get_frame_buffer, + .get_frame_buffer_count = ili9881c_get_frame_buffer_count, + .get_backlight = ili9881c_get_backlight, + .has_capability = nullptr, +}; + +Driver ili9881c_driver = { + .name = "ili9881c", + .compatible = (const char*[]) { "ilitek,ili9881c", nullptr }, + .start_device = start, + .stop_device = stop, + .api = &ili9881c_display_api, + .device_type = &DISPLAY_TYPE, + .owner = &ili9881c_module, + .internal = nullptr +}; + +#endif // SOC_MIPI_DSI_SUPPORTED diff --git a/Drivers/ili9881c-module/source/module.cpp b/Drivers/ili9881c-module/source/module.cpp new file mode 100644 index 000000000..c8b799271 --- /dev/null +++ b/Drivers/ili9881c-module/source/module.cpp @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include + +extern "C" { + +extern Driver ili9881c_driver; + +static error_t start() { + /* We crash when construct fails, because if a single driver fails to construct, + * there is no guarantee that the previously constructed drivers can be destroyed */ + check(driver_construct_add(&ili9881c_driver) == ERROR_NONE); + return ERROR_NONE; +} + +static error_t stop() { + /* We crash when destruct fails, because if a single driver fails to destruct, + * there is no guarantee that the previously destroyed drivers can be recovered */ + check(driver_remove_destruct(&ili9881c_driver) == ERROR_NONE); + return ERROR_NONE; +} + +Module ili9881c_module = { + .name = "ili9881c", + .start = start, + .stop = stop, + .symbols = nullptr, + .internal = nullptr +}; + +} // extern "C" diff --git a/Drivers/ina226-module/bindings/ti,ina226.yaml b/Drivers/ina226-module/bindings/ti,ina226.yaml index aed2c7647..bdd736bc9 100644 --- a/Drivers/ina226-module/bindings/ti,ina226.yaml +++ b/Drivers/ina226-module/bindings/ti,ina226.yaml @@ -10,3 +10,16 @@ properties: required: true # shunt value should be a minimum of 1 description: Shunt resistor value in milliohms + power-supply: + type: boolean + default: false + description: Expose a power-supply device that reports battery voltage/capacity read from the INA226's bus voltage input + power-supply-reference-voltage-mv: + type: int + default: 4200 + description: Battery voltage (in mV) considered 100% capacity, used to compute POWER_SUPPLY_PROP_CAPACITY + power-supply-min-voltage-mv: + type: int + default: 3200 + description: Battery voltage (in mV) considered 0% capacity, used to compute POWER_SUPPLY_PROP_CAPACITY. + Depends on the pack's cell count - the default is a rough 1S LiPo floor. diff --git a/Drivers/ina226-module/include/drivers/ina226.h b/Drivers/ina226-module/include/drivers/ina226.h index cdb8b026c..a2930e96c 100644 --- a/Drivers/ina226-module/include/drivers/ina226.h +++ b/Drivers/ina226-module/include/drivers/ina226.h @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once +#include #include #include @@ -13,6 +14,12 @@ extern "C" { struct Ina226Config { uint8_t address; uint16_t shunt_milliohms; + /** Expose a power-supply child device that reports battery voltage/capacity off the bus voltage reading */ + bool power_supply; + /** Battery voltage (mV) considered 100% capacity, used to derive POWER_SUPPLY_PROP_CAPACITY */ + uint32_t power_supply_reference_voltage_mv; + /** Battery voltage (mV) considered 0% capacity, used to derive POWER_SUPPLY_PROP_CAPACITY */ + uint32_t power_supply_min_voltage_mv; }; /** Bus voltage in volts (1.25 mV/LSB) */ diff --git a/Drivers/ina226-module/source/ina226.cpp b/Drivers/ina226-module/source/ina226.cpp index 460591856..2a51f1caf 100644 --- a/Drivers/ina226-module/source/ina226.cpp +++ b/Drivers/ina226-module/source/ina226.cpp @@ -1,11 +1,16 @@ // SPDX-License-Identifier: Apache-2.0 #include #include +#include #include #include +#include #include -#define TAG "INA226" +#include +#include + +constexpr auto* TAG = "INA226"; // --------------------------------------------------------------------------- // Register map @@ -30,6 +35,124 @@ static constexpr TickType_t TIMEOUT = pdMS_TO_TICKS(50); #define GET_CONFIG(device) (static_cast((device)->config)) +struct Ina226Internal { + Device* power_supply_device; +}; + +// --------------------------------------------------------------------------- +// Power supply +// --------------------------------------------------------------------------- + +static bool ps_supports_property(Device*, PowerSupplyProperty property) { + return property == POWER_SUPPLY_PROP_VOLTAGE || property == POWER_SUPPLY_PROP_CAPACITY; +} + +// battery_mv <= min_mv -> 0%, battery_mv >= reference_mv -> 100%, linear in between. min_mv is +// configurable (power-supply-min-voltage-mv) since it depends on the pack's cell count - a 1S +// LiPo's empty floor is very different from e.g. a 2S pack's. +static int estimate_capacity_from_mv(int battery_mv, uint32_t min_mv, uint32_t reference_mv) { + if ((uint32_t)battery_mv <= min_mv) return 0; + if ((uint32_t)battery_mv >= reference_mv) return 100; + return (battery_mv - (int)min_mv) * 100 / ((int)reference_mv - (int)min_mv); +} + +static error_t ps_get_property(Device* device, PowerSupplyProperty property, PowerSupplyPropertyValue* out_value) { + if (property != POWER_SUPPLY_PROP_VOLTAGE && property != POWER_SUPPLY_PROP_CAPACITY) { + return ERROR_NOT_SUPPORTED; + } + + auto* parent = device_get_parent(device); + + float volts; + error_t error = ina226_read_bus_voltage(parent, &volts); + if (error != ERROR_NONE) { + return error; + } + int battery_mv = static_cast(volts * 1000.0f); + + const auto* parent_config = GET_CONFIG(parent); + out_value->int_value = (property == POWER_SUPPLY_PROP_VOLTAGE) + ? battery_mv + : estimate_capacity_from_mv(battery_mv, parent_config->power_supply_min_voltage_mv, parent_config->power_supply_reference_voltage_mv); + return ERROR_NONE; +} + +static bool ps_supports_charge_control(Device*) { return false; } +static bool ps_is_allowed_to_charge(Device*) { return false; } +static error_t ps_set_allowed_to_charge(Device*, bool) { return ERROR_NOT_SUPPORTED; } +static bool ps_supports_quick_charge(Device*) { return false; } +static bool ps_is_quick_charge_enabled(Device*) { return false; } +static error_t ps_set_quick_charge_enabled(Device*, bool) { return ERROR_NOT_SUPPORTED; } +static bool ps_supports_power_off(Device*) { return false; } +static error_t ps_power_off(Device*) { return ERROR_NOT_SUPPORTED; } + +static constexpr PowerSupplyApi INA226_POWER_SUPPLY_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, +}; + +// Registered (driver_construct_add() in module.cpp) so driver_bind() has a valid ->internal, +// but never matched against a devicetree node: ina226 wires it up directly by pointer. +Driver ina226_power_supply_driver = { + .name = "ina226-power-supply", + .compatible = (const char*[]) { "ina226-power-supply", nullptr }, + .start_device = nullptr, + .stop_device = nullptr, + .api = &INA226_POWER_SUPPLY_API, + .device_type = &POWER_SUPPLY_TYPE, + .owner = &ina226_module, + .internal = nullptr +}; + +static error_t create_power_supply_child(Device* parent, Device*& out_child) { + auto* child = new(std::nothrow) Device { .address = 0, .name = "ina226-power-supply", .config = nullptr, .parent = nullptr, .internal = nullptr }; + if (child == nullptr) { + return ERROR_OUT_OF_MEMORY; + } + + error_t error = device_construct(child); + if (error != ERROR_NONE) { + delete child; + return error; + } + + device_set_parent(child, parent); + device_set_driver(child, &ina226_power_supply_driver); + + error = device_add(child); + if (error != ERROR_NONE) { + device_destruct(child); + delete child; + return error; + } + + error = device_start(child); + if (error != ERROR_NONE) { + device_remove(child); + device_destruct(child); + delete child; + return error; + } + + out_child = child; + return ERROR_NONE; +} + +static void destroy_power_supply_child(Device* child) { + check(device_stop(child) == ERROR_NONE); + check(device_remove(child) == ERROR_NONE); + check(device_destruct(child) == ERROR_NONE); + delete child; +} + // --------------------------------------------------------------------------- // Driver lifecycle // --------------------------------------------------------------------------- @@ -77,6 +200,23 @@ static error_t start(Device* device) { return ERROR_RESOURCE; } + auto* internal = static_cast(malloc(sizeof(Ina226Internal))); + if (internal == nullptr) { + return ERROR_OUT_OF_MEMORY; + } + internal->power_supply_device = nullptr; + device_set_driver_data(device, internal); + + if (GET_CONFIG(device)->power_supply) { + error_t error = create_power_supply_child(device, internal->power_supply_device); + if (error != ERROR_NONE) { + LOG_E(TAG, "Failed to create power-supply device"); + free(internal); + device_set_driver_data(device, nullptr); + return error; + } + } + LOG_I(TAG, "INA226 started (addr=0x%02X, shunt=%umΩ, cal=0x%04X)", addr, GET_CONFIG(device)->shunt_milliohms, cal_value); return ERROR_NONE; } @@ -88,6 +228,12 @@ static error_t stop(Device* device) { return ERROR_RESOURCE; } + auto* internal = static_cast(device_get_driver_data(device)); + if (internal->power_supply_device != nullptr) { + destroy_power_supply_child(internal->power_supply_device); + internal->power_supply_device = nullptr; + } + auto addr = GET_CONFIG(device)->address; // Put device into shutdown mode to save power @@ -95,6 +241,9 @@ static error_t stop(Device* device) { LOG_E(TAG, "Failed to shutdown INA226 (ignored)"); } + free(internal); + device_set_driver_data(device, nullptr); + return ERROR_NONE; } diff --git a/Drivers/ina226-module/source/module.cpp b/Drivers/ina226-module/source/module.cpp index 0708b24ce..1a5c5c4a8 100644 --- a/Drivers/ina226-module/source/module.cpp +++ b/Drivers/ina226-module/source/module.cpp @@ -6,17 +6,20 @@ extern "C" { extern Driver ina226_driver; +extern Driver ina226_power_supply_driver; static error_t start() { /* We crash when construct fails, because if a single driver fails to construct, * there is no guarantee that the previously constructed drivers can be destroyed */ check(driver_construct_add(&ina226_driver) == ERROR_NONE); + check(driver_construct_add(&ina226_power_supply_driver) == ERROR_NONE); return ERROR_NONE; } static error_t stop() { /* We crash when destruct fails, because if a single driver fails to destruct, * there is no guarantee that the previously destroyed drivers can be recovered */ + check(driver_remove_destruct(&ina226_power_supply_driver) == ERROR_NONE); check(driver_remove_destruct(&ina226_driver) == ERROR_NONE); return ERROR_NONE; } diff --git a/Drivers/jd9853-module/CMakeLists.txt b/Drivers/jd9853-module/CMakeLists.txt new file mode 100644 index 000000000..558c8f620 --- /dev/null +++ b/Drivers/jd9853-module/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.20) + +include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake") + +file(GLOB_RECURSE SOURCE_FILES "source/*.c*") + +tactility_add_module(jd9853-module + SRCS ${SOURCE_FILES} + INCLUDE_DIRS include/ + REQUIRES TactilityKernel platform-esp32 JD9853 esp_lcd driver +) diff --git a/Drivers/jd9853-module/LICENSE-Apache-2.0.md b/Drivers/jd9853-module/LICENSE-Apache-2.0.md new file mode 100644 index 000000000..f5f4b8b5e --- /dev/null +++ b/Drivers/jd9853-module/LICENSE-Apache-2.0.md @@ -0,0 +1,195 @@ +Apache License +============== + +_Version 2.0, January 2004_ +_<>_ + +### Terms and Conditions for use, reproduction, and distribution + +#### 1. Definitions + +“License” shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +“Licensor” shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +“Legal Entity” shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, “control” means **(i)** the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the +outstanding shares, or **(iii)** beneficial ownership of such entity. + +“You” (or “Your”) shall mean an individual or Legal Entity exercising +permissions granted by this License. + +“Source” form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +“Object” form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +“Work” shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +“Derivative Works” shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +“Contribution” shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +“submitted” means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as “Not a Contribution.” + +“Contributor” shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +#### 2. Grant of Copyright License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +#### 3. Grant of Patent License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +#### 4. Redistribution + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +* **(a)** You must give any other recipients of the Work or Derivative Works a copy of +this License; and +* **(b)** You must cause any modified files to carry prominent notices stating that You +changed the files; and +* **(c)** You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. + +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +#### 5. Submission of Contributions + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +#### 6. Trademarks + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +#### 7. Disclaimer of Warranty + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an “AS IS” BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +#### 8. Limitation of Liability + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +#### 9. Accepting Warranty or Additional Liability + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +_END OF TERMS AND CONDITIONS_ + +### APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets `[]` replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same “printed page” as the copyright notice for easier identification within +third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/Drivers/jd9853-module/README.md b/Drivers/jd9853-module/README.md new file mode 100644 index 000000000..7385cc579 --- /dev/null +++ b/Drivers/jd9853-module/README.md @@ -0,0 +1,5 @@ +# JD9853 display + +A driver for the `JD9853` display panel by Jadard, over SPI. + +License: [Apache v2.0](LICENSE-Apache-2.0.md) diff --git a/Drivers/jd9853-module/bindings/jadard,jd9853.yaml b/Drivers/jd9853-module/bindings/jadard,jd9853.yaml new file mode 100644 index 000000000..03743ff81 --- /dev/null +++ b/Drivers/jd9853-module/bindings/jadard,jd9853.yaml @@ -0,0 +1,73 @@ +description: Jadard JD9853 display panel + +bus: spi + +compatible: "jadard,jd9853" + +properties: + horizontal-resolution: + type: int + required: true + description: Horizontal resolution in pixels + vertical-resolution: + type: int + required: true + description: Vertical resolution in pixels + gap-x: + type: int + default: 0 + description: X offset applied to all draw operations + gap-y: + type: int + default: 0 + description: Y offset applied to all draw operations + swap-xy: + type: boolean + default: false + description: Swap the X and Y axes + mirror-x: + type: boolean + default: false + description: Mirror the X axis + mirror-y: + type: boolean + default: false + description: Mirror the Y axis + invert-color: + type: boolean + default: false + description: Invert the panel's color output + bgr-order: + type: boolean + default: false + description: Use BGR element order instead of RGB + pixel-clock-hz: + type: int + default: 40000000 + description: SPI pixel clock frequency in Hz + transaction-queue-depth: + type: int + default: 10 + description: Size of the internal SPI transaction queue + gamma-curve: + type: int + default: 1 + min: 0 + max: 3 + description: Gamma curve preset index [0,3], sent via the MIPI DCS GAMSET (0x26) command at bring-up + pin-dc: + type: phandles + required: true + description: Data/Command GPIO pin + pin-reset: + type: phandles + default: GPIO_PIN_SPEC_NONE + description: Reset GPIO pin + reset-active-high: + type: boolean + default: false + description: Whether the reset pin is active high + backlight: + type: phandle + default: "NULL" + description: Optional reference to this display's backlight device diff --git a/Drivers/jd9853-module/devicetree.yaml b/Drivers/jd9853-module/devicetree.yaml new file mode 100644 index 000000000..a07d6f334 --- /dev/null +++ b/Drivers/jd9853-module/devicetree.yaml @@ -0,0 +1,3 @@ +dependencies: + - TactilityKernel +bindings: bindings diff --git a/Drivers/jd9853-module/include/bindings/jd9853.h b/Drivers/jd9853-module/include/bindings/jd9853.h new file mode 100644 index 000000000..1e7afef71 --- /dev/null +++ b/Drivers/jd9853-module/include/bindings/jd9853.h @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include + +DEFINE_DEVICETREE(jd9853, struct Jd9853Config) diff --git a/Drivers/jd9853-module/include/drivers/jd9853.h b/Drivers/jd9853-module/include/drivers/jd9853.h new file mode 100644 index 000000000..ff4fde4ca --- /dev/null +++ b/Drivers/jd9853-module/include/drivers/jd9853.h @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include +#include + +struct Jd9853Config { + uint16_t horizontal_resolution; + uint16_t vertical_resolution; + int32_t gap_x; + int32_t gap_y; + bool swap_xy; + bool mirror_x; + bool mirror_y; + bool invert_color; + bool bgr_order; + uint32_t pixel_clock_hz; + uint8_t transaction_queue_depth; + // Gamma curve preset index [0,3], sent via the MIPI DCS GAMSET (0x26) command at bring-up. + uint8_t gamma_curve; + struct GpioPinSpec pin_dc; + struct GpioPinSpec pin_reset; + bool reset_active_high; + // Optional reference to this display's backlight device, NULL if none. + struct Device* backlight; +}; + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/jd9853-module/include/jd9853_module.h b/Drivers/jd9853-module/include/jd9853_module.h new file mode 100644 index 000000000..8828f95b2 --- /dev/null +++ b/Drivers/jd9853-module/include/jd9853_module.h @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +extern struct Module jd9853_module; + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/jd9853-module/source/jd9853.cpp b/Drivers/jd9853-module/source/jd9853.cpp new file mode 100644 index 000000000..c34e33149 --- /dev/null +++ b/Drivers/jd9853-module/source/jd9853.cpp @@ -0,0 +1,335 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include + +#include + +constexpr auto* TAG = "JD9853"; +#define GET_CONFIG(device) (static_cast((device)->config)) + +// Maps gamma-curve devicetree index [0,3] to the MIPI DCS GAMSET (0x26) parameter value. Mirrors +// the deprecated HAL's Jd9853Display::setGammaCurve() (Drivers/JD9853) - note the non-linear +// mapping, not index+1. +static const uint8_t GAMMA_CURVE_VALUES[4] = { 0x01, 0x04, 0x02, 0x08 }; + +struct Jd9853Internal { + esp_lcd_panel_io_handle_t io_handle; + esp_lcd_panel_handle_t panel_handle; + // See st7796-module's identical field for why this exists: draw_bitmap() must block until + // the transfer physically completes (DisplayApi's synchronous contract). + SemaphoreHandle_t draw_done_semaphore; +}; + +static bool IRAM_ATTR on_color_trans_done(esp_lcd_panel_io_handle_t, esp_lcd_panel_io_event_data_t*, void* user_ctx) { + auto* internal = static_cast(user_ctx); + BaseType_t high_task_woken = pdFALSE; + xSemaphoreGiveFromISR(internal->draw_done_semaphore, &high_task_woken); + return high_task_woken == pdTRUE; +} + +static int pin_or_unused(const struct GpioPinSpec& pin) { + return pin.gpio_controller == nullptr ? -1 : static_cast(pin.pin); +} + +// region Driver lifecycle + +static error_t start(Device* device) { + auto* parent = device_get_parent(device); + check(device_get_type(parent) == &SPI_CONTROLLER_TYPE); + + const auto* spi_config = static_cast(parent->config); + const auto* config = GET_CONFIG(device); + + struct GpioPinSpec cs_pin; + if (esp32_spi_get_cs_pin(device, &cs_pin) != ERROR_NONE) { + LOG_E(TAG, "Failed to resolve CS pin"); + return ERROR_RESOURCE; + } + + auto* internal = static_cast(malloc(sizeof(Jd9853Internal))); + if (internal == nullptr) { + return ERROR_OUT_OF_MEMORY; + } + + internal->draw_done_semaphore = xSemaphoreCreateBinary(); + if (internal->draw_done_semaphore == nullptr) { + free(internal); + return ERROR_OUT_OF_MEMORY; + } + + esp_lcd_panel_io_spi_config_t io_config = { + .cs_gpio_num = pin_or_unused(cs_pin), + .dc_gpio_num = pin_or_unused(config->pin_dc), + .spi_mode = 0, + .pclk_hz = config->pixel_clock_hz, + .trans_queue_depth = config->transaction_queue_depth, + .on_color_trans_done = on_color_trans_done, + .user_ctx = internal, + .lcd_cmd_bits = 8, + .lcd_param_bits = 8, + .cs_ena_pretrans = 0, + .cs_ena_posttrans = 0, + .flags = { + .dc_high_on_cmd = 0, + .dc_low_on_data = 0, + .dc_low_on_param = 0, + .octal_mode = 0, + .quad_mode = 0, + .sio_mode = 0, + .lsb_first = 0, + .cs_high_active = 0, + }, + }; + + esp_err_t ret = esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)spi_config->host, &io_config, &internal->io_handle); + if (ret != ESP_OK) { + LOG_E(TAG, "Failed to create panel IO: %s", esp_err_to_name(ret)); + vSemaphoreDelete(internal->draw_done_semaphore); + free(internal); + return ERROR_RESOURCE; + } + + jd9853_vendor_config_t vendor_config = { + .init_cmds = nullptr, + .init_cmds_size = 0, + }; + + esp_lcd_panel_dev_config_t panel_config = { + .reset_gpio_num = pin_or_unused(config->pin_reset), + .rgb_ele_order = config->bgr_order ? LCD_RGB_ELEMENT_ORDER_BGR : LCD_RGB_ELEMENT_ORDER_RGB, + .data_endian = LCD_RGB_DATA_ENDIAN_LITTLE, + .bits_per_pixel = 16, + .flags = { .reset_active_high = config->reset_active_high }, + .vendor_config = &vendor_config, + }; + + ret = esp_lcd_new_panel_jd9853(internal->io_handle, &panel_config, &internal->panel_handle); + if (ret != ESP_OK) { + LOG_E(TAG, "Failed to create panel: %s", esp_err_to_name(ret)); + esp_lcd_panel_io_del(internal->io_handle); + vSemaphoreDelete(internal->draw_done_semaphore); + free(internal); + return ERROR_RESOURCE; + } + + // Bring-up sequence, order matches the deprecated HAL's Jd9853Display (proven correct on real + // Waveshare S3 Touch LCD 1.47 hardware). Every failure path below must clean up fully: unlike + // stop_device, this is never retried by the kernel if start_device fails. + bool ok = + esp_lcd_panel_reset(internal->panel_handle) == ESP_OK && + esp_lcd_panel_init(internal->panel_handle) == ESP_OK && + (!config->invert_color || esp_lcd_panel_invert_color(internal->panel_handle, true) == ESP_OK); + + if (ok) { + ok = (config->gap_x == 0 && config->gap_y == 0) || esp_lcd_panel_set_gap(internal->panel_handle, config->gap_x, config->gap_y) == ESP_OK; + } + ok = ok && (!config->swap_xy || esp_lcd_panel_swap_xy(internal->panel_handle, true) == ESP_OK); + ok = ok && ((!config->mirror_x && !config->mirror_y) || esp_lcd_panel_mirror(internal->panel_handle, config->mirror_x, config->mirror_y) == ESP_OK); + ok = ok && (!config->invert_color || esp_lcd_panel_invert_color(internal->panel_handle, true) == ESP_OK); + ok = ok && (config->gamma_curve >= 4 || esp_lcd_panel_io_tx_param(internal->io_handle, LCD_CMD_GAMSET, &GAMMA_CURVE_VALUES[config->gamma_curve], 1) == ESP_OK); + ok = ok && esp_lcd_panel_disp_on_off(internal->panel_handle, true) == ESP_OK; + + if (!ok) { + LOG_E(TAG, "Failed to bring up panel"); + esp_lcd_panel_del(internal->panel_handle); + esp_lcd_panel_io_del(internal->io_handle); + vSemaphoreDelete(internal->draw_done_semaphore); + free(internal); + return ERROR_RESOURCE; + } + + device_set_driver_data(device, internal); + return ERROR_NONE; +} + +static error_t stop(Device* device) { + auto* internal = static_cast(device_get_driver_data(device)); + + if (internal->panel_handle != nullptr) { + if (esp_lcd_panel_del(internal->panel_handle) != ESP_OK) { + LOG_E(TAG, "Failed to delete panel"); + return ERROR_RESOURCE; + } + internal->panel_handle = nullptr; + } + + if (internal->io_handle != nullptr) { + if (esp_lcd_panel_io_del(internal->io_handle) != ESP_OK) { + LOG_E(TAG, "Failed to delete panel IO"); + return ERROR_RESOURCE; + } + internal->io_handle = nullptr; + } + + vSemaphoreDelete(internal->draw_done_semaphore); + free(internal); + device_set_driver_data(device, nullptr); + return ERROR_NONE; +} + +// endregion + +// region DisplayApi + +static error_t jd9853_reset(Device* device) { + auto* internal = static_cast(device_get_driver_data(device)); + return esp_lcd_panel_reset(internal->panel_handle) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE; +} + +static error_t jd9853_init(Device* device) { + auto* internal = static_cast(device_get_driver_data(device)); + return esp_lcd_panel_init(internal->panel_handle) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE; +} + +static error_t jd9853_draw_bitmap(Device* device, int32_t x_start, int32_t y_start, int32_t x_end, int32_t y_end, const void* color_data) { + auto* internal = static_cast(device_get_driver_data(device)); + + xSemaphoreTake(internal->draw_done_semaphore, 0); + + if (esp_lcd_panel_draw_bitmap(internal->panel_handle, x_start, y_start, x_end, y_end, color_data) != ESP_OK) { + return ERROR_RESOURCE; + } + + xSemaphoreTake(internal->draw_done_semaphore, portMAX_DELAY); + return ERROR_NONE; +} + +static error_t jd9853_mirror(Device* device, bool x_axis, bool y_axis) { + auto* internal = static_cast(device_get_driver_data(device)); + return esp_lcd_panel_mirror(internal->panel_handle, x_axis, y_axis) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE; +} + +static error_t jd9853_swap_xy(Device* device, bool swap_axes) { + auto* internal = static_cast(device_get_driver_data(device)); + return esp_lcd_panel_swap_xy(internal->panel_handle, swap_axes) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE; +} + +static bool jd9853_get_swap_xy(Device* device) { + return GET_CONFIG(device)->swap_xy; +} + +static bool jd9853_get_mirror_x(Device* device) { + return GET_CONFIG(device)->mirror_x; +} + +static bool jd9853_get_mirror_y(Device* device) { + return GET_CONFIG(device)->mirror_y; +} + +static error_t jd9853_set_gap(Device* device, int32_t x_gap, int32_t y_gap) { + auto* internal = static_cast(device_get_driver_data(device)); + return esp_lcd_panel_set_gap(internal->panel_handle, x_gap, y_gap) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE; +} + +static int32_t jd9853_get_gap_x(Device* device) { + return GET_CONFIG(device)->gap_x; +} + +static int32_t jd9853_get_gap_y(Device* device) { + return GET_CONFIG(device)->gap_y; +} + +static error_t jd9853_invert_color(Device* device, bool invert_color_data) { + auto* internal = static_cast(device_get_driver_data(device)); + return esp_lcd_panel_invert_color(internal->panel_handle, invert_color_data) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE; +} + +static error_t jd9853_disp_on_off(Device* device, bool on_off) { + auto* internal = static_cast(device_get_driver_data(device)); + return esp_lcd_panel_disp_on_off(internal->panel_handle, on_off) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE; +} + +static error_t jd9853_disp_sleep(Device* device, bool sleep) { + auto* internal = static_cast(device_get_driver_data(device)); + return esp_lcd_panel_disp_sleep(internal->panel_handle, sleep) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE; +} + +// The deprecated HAL's Jd9853Display always set swap_bytes=true on its lvgl_port config +// regardless of rgb_ele_order, so the byte-swapped format applies unconditionally here too - +// bgr_order only affects the panel's hardware element order (applied in start(), above). +static enum DisplayColorFormat jd9853_get_color_format(Device*) { + return DISPLAY_COLOR_FORMAT_RGB565_SWAPPED; +} + +static uint16_t jd9853_get_resolution_x(Device* device) { + return GET_CONFIG(device)->horizontal_resolution; +} + +static uint16_t jd9853_get_resolution_y(Device* device) { + return GET_CONFIG(device)->vertical_resolution; +} + +static void jd9853_get_frame_buffer(Device*, uint8_t, void** out_buffer) { + *out_buffer = nullptr; +} + +static uint8_t jd9853_get_frame_buffer_count(Device*) { + return 0; +} + +static error_t jd9853_get_backlight(Device* device, Device** backlight) { + auto* configured_backlight = GET_CONFIG(device)->backlight; + if (configured_backlight == nullptr) { + return ERROR_NOT_SUPPORTED; + } + *backlight = configured_backlight; + return ERROR_NONE; +} + +// endregion + +static const DisplayApi jd9853_display_api = { + .capabilities = DISPLAY_CAPABILITY_CAP_MIRROR | DISPLAY_CAPABILITY_CAP_SWAP_XY | + DISPLAY_CAPABILITY_CAP_SET_GAP | DISPLAY_CAPABILITY_INVERT_COLOR | DISPLAY_CAPABILITY_ON_OFF | + DISPLAY_CAPABILITY_SLEEP | DISPLAY_CAPABILITY_BACKLIGHT, + .reset = jd9853_reset, + .init = jd9853_init, + .draw_bitmap = jd9853_draw_bitmap, + .mirror = jd9853_mirror, + .swap_xy = jd9853_swap_xy, + .get_swap_xy = jd9853_get_swap_xy, + .get_mirror_x = jd9853_get_mirror_x, + .get_mirror_y = jd9853_get_mirror_y, + .set_gap = jd9853_set_gap, + .get_gap_x = jd9853_get_gap_x, + .get_gap_y = jd9853_get_gap_y, + .invert_color = jd9853_invert_color, + .disp_on_off = jd9853_disp_on_off, + .disp_sleep = jd9853_disp_sleep, + .get_color_format = jd9853_get_color_format, + .get_resolution_x = jd9853_get_resolution_x, + .get_resolution_y = jd9853_get_resolution_y, + .get_frame_buffer = jd9853_get_frame_buffer, + .get_frame_buffer_count = jd9853_get_frame_buffer_count, + .get_backlight = jd9853_get_backlight, + .has_capability = nullptr, +}; + +Driver jd9853_driver = { + .name = "jd9853", + .compatible = (const char*[]) { "jadard,jd9853", nullptr }, + .start_device = start, + .stop_device = stop, + .api = &jd9853_display_api, + .device_type = &DISPLAY_TYPE, + .owner = &jd9853_module, + .internal = nullptr +}; diff --git a/Drivers/jd9853-module/source/module.cpp b/Drivers/jd9853-module/source/module.cpp new file mode 100644 index 000000000..94f47489e --- /dev/null +++ b/Drivers/jd9853-module/source/module.cpp @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include + +extern "C" { + +extern Driver jd9853_driver; + +static error_t start() { + /* We crash when construct fails, because if a single driver fails to construct, + * there is no guarantee that the previously constructed drivers can be destroyed */ + check(driver_construct_add(&jd9853_driver) == ERROR_NONE); + return ERROR_NONE; +} + +static error_t stop() { + /* We crash when destruct fails, because if a single driver fails to destruct, + * there is no guarantee that the previously destroyed drivers can be recovered */ + check(driver_remove_destruct(&jd9853_driver) == ERROR_NONE); + return ERROR_NONE; +} + +Module jd9853_module = { + .name = "jd9853", + .start = start, + .stop = stop, + .symbols = nullptr, + .internal = nullptr +}; + +} // extern "C" diff --git a/Drivers/m5pm1-module/bindings/m5stack,m5pm1.yaml b/Drivers/m5pm1-module/bindings/m5stack,m5pm1.yaml index b8957ec4d..e2f90f19c 100644 --- a/Drivers/m5pm1-module/bindings/m5stack,m5pm1.yaml +++ b/Drivers/m5pm1-module/bindings/m5stack,m5pm1.yaml @@ -3,3 +3,16 @@ description: M5Stack M5PM1 Power Management IC include: ["i2c-device.yaml"] compatible: "m5stack,m5pm1" + +properties: + power-supply-reference-voltage-mv: + type: int + default: 4200 + description: Battery voltage (in mV) considered 100% capacity, used to compute POWER_SUPPLY_PROP_CAPACITY + speaker-amp-enable-at-boot: + type: boolean + default: false + description: > + Enable the speaker amplifier (PM1_G3) at driver start. Off by default -- the pin is + configured but left low -- to avoid a pop and needless power draw before audio starts; + boards without dynamic amp control from their audio path can turn this on. diff --git a/Drivers/m5pm1-module/include/drivers/m5pm1.h b/Drivers/m5pm1-module/include/drivers/m5pm1.h index b73e81dd4..3ca7589ce 100644 --- a/Drivers/m5pm1-module/include/drivers/m5pm1.h +++ b/Drivers/m5pm1-module/include/drivers/m5pm1.h @@ -13,6 +13,10 @@ extern "C" { struct M5pm1Config { uint8_t address; + /** Battery voltage (mV) considered 100% capacity, used to derive POWER_SUPPLY_PROP_CAPACITY */ + uint32_t power_supply_reference_voltage_mv; + /** Enable the speaker amplifier (PM1_G3) at driver start, instead of leaving it low */ + bool speaker_amp_enable_at_boot; }; // --------------------------------------------------------------------------- diff --git a/Drivers/m5pm1-module/source/m5pm1.cpp b/Drivers/m5pm1-module/source/m5pm1.cpp index ba50e354e..2c6d96c42 100644 --- a/Drivers/m5pm1-module/source/m5pm1.cpp +++ b/Drivers/m5pm1-module/source/m5pm1.cpp @@ -1,13 +1,22 @@ // SPDX-License-Identifier: Apache-2.0 #include #include +#include #include +#include #include +#include #include #include #include -#define TAG "M5PM1" +#include + +constexpr auto* TAG = "M5PM1"; + +// Rough LiPo discharge curve floor, used together with the configured reference voltage +// (the 100% point) to estimate a charge percentage from the sensed battery voltage. +static constexpr uint16_t POWER_SUPPLY_MIN_MV = 3200; // --------------------------------------------------------------------------- // Register map @@ -57,6 +66,127 @@ static constexpr TickType_t TIMEOUT = pdMS_TO_TICKS(50); #define GET_CONFIG(device) (static_cast((device)->config)) +// --------------------------------------------------------------------------- +// Power supply child device +// --------------------------------------------------------------------------- + +static int estimate_capacity_from_mv(uint16_t battery_mv, uint32_t reference_mv) { + if (battery_mv <= POWER_SUPPLY_MIN_MV) return 0; + if (battery_mv >= reference_mv) return 100; + return (battery_mv - POWER_SUPPLY_MIN_MV) * 100 / ((int)reference_mv - POWER_SUPPLY_MIN_MV); +} + +static bool ps_supports_property(Device*, PowerSupplyProperty property) { + return property == POWER_SUPPLY_PROP_IS_CHARGING || + property == POWER_SUPPLY_PROP_VOLTAGE || + property == POWER_SUPPLY_PROP_CAPACITY; +} + +static error_t ps_get_property(Device* device, PowerSupplyProperty property, PowerSupplyPropertyValue* out_value) { + // device_get_parent() here is the m5pm1 device itself (this child's parent), not the I2C bus. + auto* parent = device_get_parent(device); + + if (property == POWER_SUPPLY_PROP_IS_CHARGING) { + bool charging; + error_t error = m5pm1_is_charging(parent, &charging); + if (error != ERROR_NONE) return error; + out_value->int_value = charging ? 1 : 0; + return ERROR_NONE; + } + + if (property == POWER_SUPPLY_PROP_VOLTAGE || property == POWER_SUPPLY_PROP_CAPACITY) { + uint16_t battery_mv; + error_t error = m5pm1_get_battery_voltage(parent, &battery_mv); + if (error != ERROR_NONE) return error; + out_value->int_value = (property == POWER_SUPPLY_PROP_VOLTAGE) + ? battery_mv + : estimate_capacity_from_mv(battery_mv, GET_CONFIG(parent)->power_supply_reference_voltage_mv); + return ERROR_NONE; + } + + return ERROR_NOT_SUPPORTED; +} + +static bool ps_supports_charge_control(Device*) { return false; } +static bool ps_is_allowed_to_charge(Device*) { return false; } +static error_t ps_set_allowed_to_charge(Device*, bool) { return ERROR_NOT_SUPPORTED; } +static bool ps_supports_quick_charge(Device*) { return false; } +static bool ps_is_quick_charge_enabled(Device*) { return false; } +static error_t ps_set_quick_charge_enabled(Device*, bool) { return ERROR_NOT_SUPPORTED; } +static bool ps_supports_power_off(Device*) { return true; } +static error_t ps_power_off(Device* device) { return m5pm1_shutdown(device_get_parent(device)); } + +static constexpr PowerSupplyApi M5PM1_POWER_SUPPLY_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, +}; + +// Registered (driver_construct_add() in module.cpp) so driver_bind() has a valid ->internal, +// but never matched against a devicetree node: m5pm1_driver wires it up directly by pointer. +Driver m5pm1_power_supply_driver = { + .name = "m5pm1-power-supply", + .compatible = (const char*[]) { "m5pm1-power-supply", nullptr }, + .start_device = nullptr, + .stop_device = nullptr, + .api = &M5PM1_POWER_SUPPLY_API, + .device_type = &POWER_SUPPLY_TYPE, + .owner = &m5pm1_module, + .internal = nullptr +}; + +struct M5pm1Internal { + Device* power_supply_device = nullptr; +}; + +static error_t create_power_supply_child(Device* parent, Device*& out_child) { + auto* child = new(std::nothrow) Device { .address = 0, .name = "m5pm1-power-supply", .config = nullptr, .parent = nullptr, .internal = nullptr }; + if (child == nullptr) { + return ERROR_OUT_OF_MEMORY; + } + + error_t error = device_construct(child); + if (error != ERROR_NONE) { + delete child; + return error; + } + + device_set_parent(child, parent); + device_set_driver(child, &m5pm1_power_supply_driver); + + error = device_add(child); + if (error != ERROR_NONE) { + device_destruct(child); + delete child; + return error; + } + + error = device_start(child); + if (error != ERROR_NONE) { + device_remove(child); + device_destruct(child); + delete child; + return error; + } + + out_child = child; + return ERROR_NONE; +} + +static void destroy_power_supply_child(Device* child) { + check(device_stop(child) == ERROR_NONE); + check(device_remove(child) == ERROR_NONE); + check(device_destruct(child) == ERROR_NONE); + delete child; +} + // --------------------------------------------------------------------------- // Driver lifecycle // --------------------------------------------------------------------------- @@ -83,6 +213,19 @@ static error_t start(Device* device) { vTaskDelay(pdMS_TO_TICKS(20 * (attempt + 1))); } + auto* internal = new(std::nothrow) M5pm1Internal(); + if (internal == nullptr) { + return ERROR_OUT_OF_MEMORY; + } + + error_t error = create_power_supply_child(device, internal->power_supply_device); + if (error != ERROR_NONE) { + delete internal; + return error; + } + + device_set_driver_data(device, internal); + if (!awake) { LOG_E(TAG, "M5PM1 not responding — LCD power will not be enabled"); return ERROR_NONE; // non-fatal: don't crash the kernel @@ -126,10 +269,22 @@ static error_t start(Device* device) { LOG_W(TAG, "Failed to configure speaker amp pin"); } + if (spk_ok && GET_CONFIG(device)->speaker_amp_enable_at_boot) { + if (i2c_controller_register8_set_bits(i2c, addr, REG_GPIO_OUT, SPEAKER_AMP_BIT, TIMEOUT) == ERROR_NONE) { + LOG_I(TAG, "Speaker amp enabled at boot"); + } else { + LOG_W(TAG, "Failed to enable speaker amp at boot"); + } + } + return ERROR_NONE; } static error_t stop(Device* device) { + auto* internal = static_cast(device_get_driver_data(device)); + destroy_power_supply_child(internal->power_supply_device); + device_set_driver_data(device, nullptr); + delete internal; return ERROR_NONE; } diff --git a/Drivers/m5pm1-module/source/module.cpp b/Drivers/m5pm1-module/source/module.cpp index 1c5fe6fb7..eca02bf5e 100644 --- a/Drivers/m5pm1-module/source/module.cpp +++ b/Drivers/m5pm1-module/source/module.cpp @@ -6,17 +6,20 @@ extern "C" { extern Driver m5pm1_driver; +extern Driver m5pm1_power_supply_driver; static error_t start() { /* We crash when construct fails, because if a single driver fails to construct, * there is no guarantee that the previously constructed drivers can be destroyed */ check(driver_construct_add(&m5pm1_driver) == ERROR_NONE); + check(driver_construct_add(&m5pm1_power_supply_driver) == ERROR_NONE); return ERROR_NONE; } static error_t stop() { /* We crash when destruct fails, because if a single driver fails to destruct, * there is no guarantee that the previously destroyed drivers can be recovered */ + check(driver_remove_destruct(&m5pm1_power_supply_driver) == ERROR_NONE); check(driver_remove_destruct(&m5pm1_driver) == ERROR_NONE); return ERROR_NONE; } diff --git a/Drivers/st7123-module/CMakeLists.txt b/Drivers/st7123-module/CMakeLists.txt new file mode 100644 index 000000000..4198a4ae4 --- /dev/null +++ b/Drivers/st7123-module/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.20) + +include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake") + +file(GLOB_RECURSE SOURCE_FILES "source/*.c*") + +tactility_add_module(st7123-module + SRCS ${SOURCE_FILES} + INCLUDE_DIRS include/ + REQUIRES TactilityKernel platform-esp32 esp_lcd_st7123 esp_lcd_touch_st7123 esp_lcd driver esp_hw_support +) diff --git a/Drivers/st7123-module/LICENSE-Apache-2.0.md b/Drivers/st7123-module/LICENSE-Apache-2.0.md new file mode 100644 index 000000000..f5f4b8b5e --- /dev/null +++ b/Drivers/st7123-module/LICENSE-Apache-2.0.md @@ -0,0 +1,195 @@ +Apache License +============== + +_Version 2.0, January 2004_ +_<>_ + +### Terms and Conditions for use, reproduction, and distribution + +#### 1. Definitions + +“License” shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +“Licensor” shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +“Legal Entity” shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, “control” means **(i)** the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the +outstanding shares, or **(iii)** beneficial ownership of such entity. + +“You” (or “Your”) shall mean an individual or Legal Entity exercising +permissions granted by this License. + +“Source” form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +“Object” form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +“Work” shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +“Derivative Works” shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +“Contribution” shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +“submitted” means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as “Not a Contribution.” + +“Contributor” shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +#### 2. Grant of Copyright License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +#### 3. Grant of Patent License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +#### 4. Redistribution + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +* **(a)** You must give any other recipients of the Work or Derivative Works a copy of +this License; and +* **(b)** You must cause any modified files to carry prominent notices stating that You +changed the files; and +* **(c)** You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. + +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +#### 5. Submission of Contributions + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +#### 6. Trademarks + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +#### 7. Disclaimer of Warranty + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an “AS IS” BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +#### 8. Limitation of Liability + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +#### 9. Accepting Warranty or Additional Liability + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +_END OF TERMS AND CONDITIONS_ + +### APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets `[]` replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same “printed page” as the copyright notice for easier identification within +third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/Drivers/st7123-module/README.md b/Drivers/st7123-module/README.md new file mode 100644 index 000000000..7afc2d324 --- /dev/null +++ b/Drivers/st7123-module/README.md @@ -0,0 +1,18 @@ +# ST7123 display + touch controller + +Two drivers for the Sitronix `ST7123`, sharing this module since they're the same physical chip +family and commonly used together: + +- **`st7123`** (`sitronix,st7123`) - the MIPI-DSI display panel itself, driven over ESP-IDF's + `esp_lcd_st7123` component (DPI/DBI interface, ESP32-P4 and other `SOC_MIPI_DSI_SUPPORTED` + targets only). Owns the MIPI DSI PHY LDO channel and DSI bus directly, so unlike SPI/RGB panels + it has no parent bus controller device. +- **`st7123-touch`** (`sitronix,st7123-touch`) - the panel's in-cell capacitive touch controller, + a separate I2C device, driven over ESP-IDF's `esp_lcd_touch_st7123` component. Sits at a fixed + I2C address (0x55) - the devicetree `reg` is documentation only, not used to probe the bus. + Reset is usually wired through the board's IO expander rather than a direct SoC GPIO (every + known user of this driver so far does it that way), hence `pin-reset` defaults unset; it's + still exposed for parity with the other touch drivers (e.g. `gt911-module`) in case a board + wires it directly. + +License: [Apache v2.0](LICENSE-Apache-2.0.md) diff --git a/Drivers/st7123-module/bindings/sitronix,st7123-touch.yaml b/Drivers/st7123-module/bindings/sitronix,st7123-touch.yaml new file mode 100644 index 000000000..e5ad40d67 --- /dev/null +++ b/Drivers/st7123-module/bindings/sitronix,st7123-touch.yaml @@ -0,0 +1,47 @@ +description: Sitronix ST7123 in-cell capacitive touch controller, driven over ESP-IDF's + esp_lcd_touch_st7123 component + +include: ["i2c-device.yaml"] + +compatible: "sitronix,st7123-touch" + +bus: i2c + +properties: + x-max: + type: int + required: true + description: Maximum X coordinate reported by the controller (typically the panel's horizontal resolution) + y-max: + type: int + required: true + description: Maximum Y coordinate reported by the controller (typically the panel's vertical resolution) + swap-xy: + type: boolean + default: false + description: Swap the X and Y axes + mirror-x: + type: boolean + default: false + description: Mirror the X axis + mirror-y: + type: boolean + default: false + description: Mirror the Y axis + pin-reset: + type: phandles + default: GPIO_PIN_SPEC_NONE + description: Reset GPIO pin. Most boards wire this through an IO expander and handle the + reset pulse themselves rather than through this driver - leave unset in that case. + pin-interrupt: + type: phandles + default: GPIO_PIN_SPEC_NONE + description: Interrupt GPIO pin + reset-active-high: + type: boolean + default: false + description: Whether the reset pin is active high + interrupt-active-high: + type: boolean + default: false + description: Whether the interrupt pin is active high diff --git a/Drivers/st7123-module/bindings/sitronix,st7123.yaml b/Drivers/st7123-module/bindings/sitronix,st7123.yaml new file mode 100644 index 000000000..6dc9f14e1 --- /dev/null +++ b/Drivers/st7123-module/bindings/sitronix,st7123.yaml @@ -0,0 +1,127 @@ +description: > + Sitronix ST7123 MIPI-DSI display panel, driven over ESP-IDF's esp_lcd_st7123 component + (DPI/DBI interface, ESP32-P4 and other SOC_MIPI_DSI_SUPPORTED targets only). Owns the MIPI DSI + PHY LDO channel and DSI bus directly, so unlike SPI/RGB panels it has no parent bus controller + device. + +compatible: "sitronix,st7123" + +properties: + horizontal-resolution: + type: int + required: true + description: Horizontal resolution in pixels + vertical-resolution: + type: int + required: true + description: Vertical resolution in pixels + bits-per-pixel: + type: int + default: 16 + description: Color depth in bits per pixel (16, 18 or 24) + bgr-order: + type: boolean + default: false + description: Use BGR element order instead of RGB + invert-color: + type: boolean + default: false + description: Invert the panel's color output + mirror-x: + type: boolean + default: false + description: Mirror the X axis + mirror-y: + type: boolean + default: false + description: Mirror the Y axis + pin-reset: + type: phandles + default: GPIO_PIN_SPEC_NONE + description: Reset GPIO pin. Falls back to a software reset (sent over the DBI command + interface) if not set. + reset-active-high: + type: boolean + default: false + description: Whether the reset pin is active high + ldo-channel: + type: int + required: true + description: LDO channel index powering the MIPI DSI PHY (chan_id in esp_ldo_channel_config_t) + ldo-voltage-mv: + type: int + required: true + description: Voltage to supply to the MIPI DSI PHY LDO channel, in mV + dsi-bus-id: + type: int + default: 0 + description: Which DSI controller to use, index from 0 + num-data-lanes: + type: int + default: 2 + description: Number of MIPI DSI data lanes. 0 falls back to the maximum available. + lane-bit-rate-mbps: + type: int + required: true + description: MIPI DSI lane bit rate, in Mbps + dpi-clock-freq-mhz: + type: int + required: true + description: MIPI DPI clock frequency, in MHz + hsync-pulse-width: + type: int + required: true + description: Horizontal sync width, in PCLK periods + hsync-back-porch: + type: int + required: true + description: Number of PCLK periods between hsync and the start of line active data + hsync-front-porch: + type: int + required: true + description: Number of PCLK periods between the end of active data and the next hsync + vsync-pulse-width: + type: int + required: true + description: Vertical sync width, in lines + vsync-back-porch: + type: int + required: true + description: Number of invalid lines between vsync and the start of the frame + vsync-front-porch: + type: int + required: true + description: Number of invalid lines between the end of the frame and the next vsync + num-fbs: + type: int + default: 1 + description: Number of screen-sized frame buffers to allocate (0 or 1 = single-buffered) + use-dma2d: + type: boolean + default: true + description: Use DMA2D to copy user buffers into the frame buffer when necessary (only + meaningful on SOC_DMA2D_SUPPORTED targets - must be false otherwise) + disable-lp: + type: boolean + default: false + description: Disable MIPI DSI low-power mode + allow-tearing: + type: boolean + default: false + description: By default, when draw_bitmap's color_data is one of the panel's own frame + buffers (i.e. LVGL is bound directly onto them), draw_bitmap waits for a full scan-out to + complete before returning so the caller can't start overwriting a buffer still being + displayed. Set this to trade away that tear-free guarantee for lower latency (e.g. when + other tasks occasionally block timing for long enough that waiting causes visible stalls). + init-sequence: + type: array + element-type: uint8_t + description: > + Custom vendor bring-up sequence, flattened into bytes as a run of + [cmd, data-length, delay-ms, data-length bytes of data...] entries, e.g. + `init-sequence = [0x11 0 120 0x29 0 20];`. + Omit to use the ST7123 component's own built-in default sequence. + backlight: + type: phandle + default: "NULL" + description: Optional reference to this display's backlight device diff --git a/Drivers/st7123-module/devicetree.yaml b/Drivers/st7123-module/devicetree.yaml new file mode 100644 index 000000000..a07d6f334 --- /dev/null +++ b/Drivers/st7123-module/devicetree.yaml @@ -0,0 +1,3 @@ +dependencies: + - TactilityKernel +bindings: bindings diff --git a/Drivers/st7123-module/include/bindings/st7123.h b/Drivers/st7123-module/include/bindings/st7123.h new file mode 100644 index 000000000..6c3c12fde --- /dev/null +++ b/Drivers/st7123-module/include/bindings/st7123.h @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include + +// The devicetree compiler derives the expected config typedef name from the compatible +// string's suffix (e.g. "sitronix,st7123" -> st7123_config_dt), not from the node name or +// driver name, so the tag here must match that exactly. +DEFINE_DEVICETREE(st7123, struct St7123Config) diff --git a/Drivers/st7123-module/include/bindings/st7123_touch.h b/Drivers/st7123-module/include/bindings/st7123_touch.h new file mode 100644 index 000000000..05f93524d --- /dev/null +++ b/Drivers/st7123-module/include/bindings/st7123_touch.h @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include + +// The devicetree compiler derives the expected config typedef name from the compatible +// string's suffix, with hyphens replaced by underscores (e.g. "sitronix,st7123-touch" -> +// st7123_touch_config_dt), not from the node name or driver name, so the tag here must match +// that exactly. +DEFINE_DEVICETREE(st7123_touch, struct St7123TouchConfig) diff --git a/Drivers/st7123-module/include/drivers/st7123.h b/Drivers/st7123-module/include/drivers/st7123.h new file mode 100644 index 000000000..1e1c0a3a5 --- /dev/null +++ b/Drivers/st7123-module/include/drivers/st7123.h @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include +#include + +struct St7123Config { + uint16_t horizontal_resolution; + uint16_t vertical_resolution; + uint8_t bits_per_pixel; + bool bgr_order; + bool invert_color; + bool mirror_x; + bool mirror_y; + + // Reset pin for the panel. GPIO_PIN_SPEC_NONE falls back to a software reset sent over the + // DBI command interface (see esp_lcd_st7123's panel_st7123_reset()). + struct GpioPinSpec pin_reset; + bool reset_active_high; + + // LDO channel powering the MIPI DSI PHY - the PHY has no power of its own until this is + // acquired, so it must happen before the DSI bus is created. Both fields are int32_t (not + // uint32_t) to exactly match esp_ldo_channel_config_t's plain `int` fields - assigning a + // uint32_t into that struct's brace-init would be a narrowing conversion (-Werror=narrowing). + int32_t ldo_channel; + int32_t ldo_voltage_mv; + + uint8_t dsi_bus_id; + // Number of MIPI DSI data lanes. 0 falls back to the maximum available. + uint8_t num_data_lanes; + uint32_t lane_bit_rate_mbps; + + uint32_t dpi_clock_freq_mhz; + uint32_t hsync_pulse_width; + uint32_t hsync_back_porch; + uint32_t hsync_front_porch; + uint32_t vsync_pulse_width; + uint32_t vsync_back_porch; + uint32_t vsync_front_porch; + // Number of screen-sized frame buffers the driver allocates (0 or 1 = single-buffered). + uint8_t num_fbs; + bool use_dma2d; + bool disable_lp; + + // See the 'allow-tearing' binding property. + bool allow_tearing; + + // Custom vendor init sequence, flattened as bytes: a run of + // [cmd, data_len, delay_ms, data_len bytes of data...] entries. NULL/0 falls back to the + // ST7123 component's own built-in default sequence (see vendor_specific_init_default in + // esp_lcd_st7123.c) - not guaranteed to match any particular panel's actual bring-up + // requirements. + const uint8_t* init_sequence; + uint32_t init_sequence_length; + + // Optional reference to this display's backlight device, NULL if none. + struct Device* backlight; +}; + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/st7123-module/include/drivers/st7123_touch.h b/Drivers/st7123-module/include/drivers/st7123_touch.h new file mode 100644 index 000000000..f4c75e2a5 --- /dev/null +++ b/Drivers/st7123-module/include/drivers/st7123_touch.h @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include + +struct St7123TouchConfig { + // Devicetree address hint. esp_lcd_touch_st7123 always talks to the controller's one fixed + // address (0x55 - see ESP_LCD_TOUCH_IO_I2C_ST7123_ADDRESS), so this isn't actually used to + // probe the bus - it just documents the wiring like any other i2c-device node. + uint8_t address; + uint16_t x_max; + uint16_t y_max; + bool swap_xy; + bool mirror_x; + bool mirror_y; + // Reset is wired through the board's IO expander rather than a direct SoC GPIO on every + // known user of this driver so far, hence GPIO_PIN_SPEC_NONE by default - but exposed here + // for parity with the other touch drivers (e.g. gt911-module) in case a board wires it directly. + struct GpioPinSpec pin_reset; + struct GpioPinSpec pin_interrupt; + bool reset_active_high; + bool interrupt_active_high; +}; + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/st7123-module/include/st7123_module.h b/Drivers/st7123-module/include/st7123_module.h new file mode 100644 index 000000000..d37648a11 --- /dev/null +++ b/Drivers/st7123-module/include/st7123_module.h @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +extern struct Module st7123_module; + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/st7123-module/source/module.cpp b/Drivers/st7123-module/source/module.cpp new file mode 100644 index 000000000..c74a4c119 --- /dev/null +++ b/Drivers/st7123-module/source/module.cpp @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include + +extern "C" { + +extern Driver st7123_driver; +extern Driver st7123_touch_driver; + +static error_t start() { + /* We crash when construct fails, because if a single driver fails to construct, + * there is no guarantee that the previously constructed drivers can be destroyed */ + check(driver_construct_add(&st7123_driver) == ERROR_NONE); + check(driver_construct_add(&st7123_touch_driver) == ERROR_NONE); + return ERROR_NONE; +} + +static error_t stop() { + /* We crash when destruct fails, because if a single driver fails to destruct, + * there is no guarantee that the previously destroyed drivers can be recovered */ + check(driver_remove_destruct(&st7123_touch_driver) == ERROR_NONE); + check(driver_remove_destruct(&st7123_driver) == ERROR_NONE); + return ERROR_NONE; +} + +Module st7123_module = { + .name = "st7123", + .start = start, + .stop = stop, + .symbols = nullptr, + .internal = nullptr +}; + +} // extern "C" diff --git a/Drivers/st7123-module/source/st7123.cpp b/Drivers/st7123-module/source/st7123.cpp new file mode 100644 index 000000000..f56e71523 --- /dev/null +++ b/Drivers/st7123-module/source/st7123.cpp @@ -0,0 +1,525 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#if SOC_MIPI_DSI_SUPPORTED + +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +constexpr auto* TAG = "ST7123"; +#define GET_CONFIG(device) (static_cast((device)->config)) + +// Generic lvgl-module display glue (Modules/lvgl-module/source/lvgl_display.c) only ever asks +// for frame buffer index 0 and 1, so caching more than that would be dead weight. +constexpr size_t MAX_CACHED_FRAME_BUFFERS = 2; + +struct St7123Internal { + esp_ldo_channel_handle_t ldo_handle; + esp_lcd_dsi_bus_handle_t dsi_bus_handle; + esp_lcd_panel_io_handle_t io_handle; + esp_lcd_panel_handle_t panel_handle; + void* frame_buffers[MAX_CACHED_FRAME_BUFFERS]; + uint8_t frame_buffer_count; + // Size of each buffer in frame_buffers, in bytes - used to range-check whether a given + // draw_bitmap() color_data pointer is actually one of them (see draw_bitmap() below). + size_t frame_buffer_size_bytes; + // Signaled by on_refresh_done once per real scan-out of a whole frame. Only waited on in + // draw_bitmap() when color_data is one of frame_buffers and avoid_tearing is set - see the + // comment there for why. + SemaphoreHandle_t frame_complete_semaphore; + // Heap-allocated only when the devicetree supplies a custom init_sequence (see + // parse_init_sequence()) - nullptr otherwise, since the vendor's built-in default sequence + // needs no parsing. Its .data pointers alias directly into the devicetree's static const + // byte buffer, so only this struct array itself needs freeing in stop(). + st7123_lcd_init_cmd_t* parsed_init_cmds; +}; + +// esp_lcd_dpi_panel's draw_bitmap() has a zero-copy path when color_data is one of the panel's +// own frame buffers (as returned by esp_lcd_dpi_panel_get_frame_buffer()): it just repoints which +// buffer is scanned out and returns almost instantly - well before the DSI peripheral has +// actually finished scanning out the *previous* buffer, let alone started on this one. Callers in +// full/direct LVGL render mode render straight into these real frame buffers, so if draw_bitmap() +// returned that quickly, LVGL would be free to start overwriting the *other* buffer - which may +// still be mid-scanout - producing visible tearing/flashing. on_refresh_done fires once per actual +// whole-frame scan-out completion (continuously, at the panel's refresh rate, independent of +// draw_bitmap calls), so waiting for the next occurrence after each draw_bitmap() genuinely blocks +// until it's safe to start writing into the frame buffers again - unless avoid_tearing is false, +// in which case the caller has opted out of this wait (see the binding property). +static bool on_refresh_done(esp_lcd_panel_handle_t, esp_lcd_dpi_panel_event_data_t*, void* user_ctx) { + auto* internal = static_cast(user_ctx); + BaseType_t high_task_woken = pdFALSE; + xSemaphoreGiveFromISR(internal->frame_complete_semaphore, &high_task_woken); + return high_task_woken == pdTRUE; +} + +static int pin_or_unused(const GpioPinSpec& pin) { + return pin.gpio_controller == nullptr ? -1 : static_cast(pin.pin); +} + +static lcd_color_format_t color_format_from_bits_per_pixel(uint8_t bits_per_pixel) { + switch (bits_per_pixel) { + case 18: return LCD_COLOR_FMT_RGB666; + case 24: return LCD_COLOR_FMT_RGB888; + default: return LCD_COLOR_FMT_RGB565; + } +} + +// Unpacks the devicetree's flat [cmd, data_len, delay_ms, data_len bytes...] encoding (produced +// by the devicetree compiler's "array" property type - see init-sequence in +// bindings/sitronix,st7123.yaml) into a heap-allocated st7123_lcd_init_cmd_t array. Each entry's +// .data points directly into `bytes`, which is the devicetree's static const buffer and outlives +// the device, so no per-entry copy is needed. +static bool parse_init_sequence(const uint8_t* bytes, uint32_t length, st7123_lcd_init_cmd_t** out_cmds, uint16_t* out_count) { + uint32_t count = 0; + for (uint32_t offset = 0; offset < length; count++) { + if (offset + 3 > length) { + LOG_E(TAG, "init-sequence truncated: entry header runs past the end of the array"); + return false; + } + offset += 3 + bytes[offset + 1]; + if (offset > length) { + LOG_E(TAG, "init-sequence truncated: entry data runs past the end of the array"); + return false; + } + } + + auto* cmds = static_cast(malloc(count * sizeof(st7123_lcd_init_cmd_t))); + if (cmds == nullptr) { + return false; + } + + uint32_t offset = 0; + for (uint32_t i = 0; i < count; i++) { + uint8_t data_len = bytes[offset + 1]; + cmds[i] = { + .cmd = bytes[offset], + .data = data_len > 0 ? &bytes[offset + 3] : nullptr, + .data_bytes = data_len, + .delay_ms = bytes[offset + 2], + }; + offset += 3 + data_len; + } + + *out_cmds = cmds; + *out_count = (uint16_t)count; + return true; +} + +// region Driver lifecycle + +static error_t start(Device* device) { + const auto* config = GET_CONFIG(device); + + auto* internal = static_cast(malloc(sizeof(St7123Internal))); + if (internal == nullptr) { + return ERROR_OUT_OF_MEMORY; + } + internal->parsed_init_cmds = nullptr; + + const st7123_lcd_init_cmd_t* init_cmds = nullptr; + uint16_t init_cmds_size = 0; + if (config->init_sequence != nullptr && config->init_sequence_length > 0) { + if (!parse_init_sequence(config->init_sequence, config->init_sequence_length, &internal->parsed_init_cmds, &init_cmds_size)) { + LOG_E(TAG, "Failed to parse init-sequence property"); + free(internal); + return ERROR_INVALID_ARGUMENT; + } + init_cmds = internal->parsed_init_cmds; + } + + // The MIPI DSI PHY has no power of its own until this LDO channel is enabled - must happen + // before the DSI bus is created. + esp_ldo_channel_config_t ldo_config = { + .chan_id = config->ldo_channel, + .voltage_mv = config->ldo_voltage_mv, + .flags = {}, + }; + if (esp_ldo_acquire_channel(&ldo_config, &internal->ldo_handle) != ESP_OK) { + LOG_E(TAG, "Failed to acquire LDO channel for MIPI DSI PHY"); + free(internal->parsed_init_cmds); + free(internal); + return ERROR_RESOURCE; + } + + const esp_lcd_dsi_bus_config_t bus_config = { + .bus_id = config->dsi_bus_id, + .num_data_lanes = config->num_data_lanes, + .phy_clk_src = MIPI_DSI_PHY_CLK_SRC_DEFAULT, + .lane_bit_rate_mbps = config->lane_bit_rate_mbps, + }; + if (esp_lcd_new_dsi_bus(&bus_config, &internal->dsi_bus_handle) != ESP_OK) { + LOG_E(TAG, "Failed to create MIPI DSI bus"); + esp_ldo_release_channel(internal->ldo_handle); + free(internal->parsed_init_cmds); + free(internal); + return ERROR_RESOURCE; + } + + const 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(internal->dsi_bus_handle, &dbi_config, &internal->io_handle) != ESP_OK) { + LOG_E(TAG, "Failed to create panel IO"); + esp_lcd_del_dsi_bus(internal->dsi_bus_handle); + esp_ldo_release_channel(internal->ldo_handle); + free(internal->parsed_init_cmds); + free(internal); + return ERROR_RESOURCE; + } + + const lcd_color_format_t color_format = color_format_from_bits_per_pixel(config->bits_per_pixel); + const esp_lcd_dpi_panel_config_t dpi_config = { + .virtual_channel = 0, + .dpi_clk_src = MIPI_DSI_DPI_CLK_SRC_DEFAULT, + .dpi_clock_freq_mhz = config->dpi_clock_freq_mhz, + .pixel_format = (lcd_color_rgb_pixel_format_t)0, // deprecated field - in/out_color_format below take precedence + .in_color_format = color_format, + .out_color_format = color_format, + .num_fbs = config->num_fbs, + .video_timing = { + .h_size = config->horizontal_resolution, + .v_size = config->vertical_resolution, + .hsync_pulse_width = config->hsync_pulse_width, + .hsync_back_porch = config->hsync_back_porch, + .hsync_front_porch = config->hsync_front_porch, + .vsync_pulse_width = config->vsync_pulse_width, + .vsync_back_porch = config->vsync_back_porch, + .vsync_front_porch = config->vsync_front_porch, + }, + .flags = { + .use_dma2d = config->use_dma2d, + .disable_lp = config->disable_lp, + }, + }; + + st7123_vendor_config_t vendor_config = { + .init_cmds = init_cmds, + .init_cmds_size = init_cmds_size, + .mipi_config = { + .dsi_bus = internal->dsi_bus_handle, + .dpi_config = &dpi_config, + }, + }; + + const esp_lcd_panel_dev_config_t panel_config = { + .reset_gpio_num = pin_or_unused(config->pin_reset), + .rgb_ele_order = config->bgr_order ? LCD_RGB_ELEMENT_ORDER_BGR : LCD_RGB_ELEMENT_ORDER_RGB, + .data_endian = LCD_RGB_DATA_ENDIAN_LITTLE, + .bits_per_pixel = config->bits_per_pixel, + .flags = { .reset_active_high = config->reset_active_high }, + .vendor_config = &vendor_config, + }; + + if (esp_lcd_new_panel_st7123(internal->io_handle, &panel_config, &internal->panel_handle) != ESP_OK) { + LOG_E(TAG, "Failed to create panel"); + esp_lcd_panel_io_del(internal->io_handle); + esp_lcd_del_dsi_bus(internal->dsi_bus_handle); + esp_ldo_release_channel(internal->ldo_handle); + free(internal->parsed_init_cmds); + free(internal); + return ERROR_RESOURCE; + } + + // Bring-up sequence: reset() pulses (or software-resets) the panel and init() pushes + // init_cmds over the DBI command interface before bringing up the underlying DPI peripheral. + // swap_xy/set_gap are intentionally not called: the ST7123 driver doesn't override them and + // the underlying raw DPI panel doesn't implement them either, so both would just fail with + // ESP_ERR_NOT_SUPPORTED (see DisplayApi below). Every failure path here must clean up fully: + // unlike stop_device, this is never retried by the kernel if start_device fails (see + // device_start() in TactilityKernel), so a partial failure here would leak. + bool ok = + esp_lcd_panel_reset(internal->panel_handle) == ESP_OK && + esp_lcd_panel_init(internal->panel_handle) == ESP_OK && + esp_lcd_panel_invert_color(internal->panel_handle, config->invert_color) == ESP_OK && + esp_lcd_panel_mirror(internal->panel_handle, config->mirror_x, config->mirror_y) == ESP_OK && + esp_lcd_panel_disp_on_off(internal->panel_handle, true) == ESP_OK; + + if (!ok) { + LOG_E(TAG, "Failed to bring up panel"); + esp_lcd_panel_del(internal->panel_handle); + esp_lcd_panel_io_del(internal->io_handle); + esp_lcd_del_dsi_bus(internal->dsi_bus_handle); + esp_ldo_release_channel(internal->ldo_handle); + free(internal->parsed_init_cmds); + free(internal); + return ERROR_RESOURCE; + } + + internal->frame_buffer_count = 0; + internal->frame_buffer_size_bytes = (size_t)config->horizontal_resolution * config->vertical_resolution * + ((config->bits_per_pixel + 7) / 8); + if (config->num_fbs > 0) { + // esp_lcd_dpi_panel_get_frame_buffer() is variadic: the number of out-pointer arguments + // passed must match fb_num exactly, so this can't be a loop. + size_t fb_num = config->num_fbs < MAX_CACHED_FRAME_BUFFERS ? config->num_fbs : MAX_CACHED_FRAME_BUFFERS; + esp_err_t ret; + switch (fb_num) { + case 1: + ret = esp_lcd_dpi_panel_get_frame_buffer(internal->panel_handle, 1, &internal->frame_buffers[0]); + break; + case 2: + ret = esp_lcd_dpi_panel_get_frame_buffer(internal->panel_handle, 2, &internal->frame_buffers[0], &internal->frame_buffers[1]); + break; + default: + ret = ESP_OK; + break; + } + + if (ret != ESP_OK) { + LOG_E(TAG, "Failed to get frame buffer(s): %s", esp_err_to_name(ret)); + esp_lcd_panel_del(internal->panel_handle); + esp_lcd_panel_io_del(internal->io_handle); + esp_lcd_del_dsi_bus(internal->dsi_bus_handle); + esp_ldo_release_channel(internal->ldo_handle); + free(internal->parsed_init_cmds); + free(internal); + return ERROR_RESOURCE; + } + internal->frame_buffer_count = (uint8_t)fb_num; + } + + internal->frame_complete_semaphore = xSemaphoreCreateBinary(); + if (internal->frame_complete_semaphore == nullptr) { + esp_lcd_panel_del(internal->panel_handle); + esp_lcd_panel_io_del(internal->io_handle); + esp_lcd_del_dsi_bus(internal->dsi_bus_handle); + esp_ldo_release_channel(internal->ldo_handle); + free(internal->parsed_init_cmds); + free(internal); + return ERROR_OUT_OF_MEMORY; + } + + esp_lcd_dpi_panel_event_callbacks_t callbacks = {}; + callbacks.on_refresh_done = on_refresh_done; + if (esp_lcd_dpi_panel_register_event_callbacks(internal->panel_handle, &callbacks, internal) != ESP_OK) { + LOG_E(TAG, "Failed to register panel event callbacks"); + vSemaphoreDelete(internal->frame_complete_semaphore); + esp_lcd_panel_del(internal->panel_handle); + esp_lcd_panel_io_del(internal->io_handle); + esp_lcd_del_dsi_bus(internal->dsi_bus_handle); + esp_ldo_release_channel(internal->ldo_handle); + free(internal->parsed_init_cmds); + free(internal); + return ERROR_RESOURCE; + } + + device_set_driver_data(device, internal); + return ERROR_NONE; +} + +static error_t stop(Device* device) { + auto* internal = static_cast(device_get_driver_data(device)); + + if (internal->panel_handle != nullptr) { + if (esp_lcd_panel_del(internal->panel_handle) != ESP_OK) { + LOG_E(TAG, "Failed to delete panel"); + return ERROR_RESOURCE; + } + internal->panel_handle = nullptr; + } + + if (internal->io_handle != nullptr) { + if (esp_lcd_panel_io_del(internal->io_handle) != ESP_OK) { + LOG_E(TAG, "Failed to delete panel IO"); + return ERROR_RESOURCE; + } + internal->io_handle = nullptr; + } + + if (internal->dsi_bus_handle != nullptr) { + if (esp_lcd_del_dsi_bus(internal->dsi_bus_handle) != ESP_OK) { + LOG_E(TAG, "Failed to delete DSI bus"); + return ERROR_RESOURCE; + } + internal->dsi_bus_handle = nullptr; + } + + if (internal->ldo_handle != nullptr) { + if (esp_ldo_release_channel(internal->ldo_handle) != ESP_OK) { + LOG_E(TAG, "Failed to release LDO channel"); + return ERROR_RESOURCE; + } + internal->ldo_handle = nullptr; + } + + vSemaphoreDelete(internal->frame_complete_semaphore); + free(internal->parsed_init_cmds); + free(internal); + device_set_driver_data(device, nullptr); + return ERROR_NONE; +} + +// endregion + +// region DisplayApi + +static error_t st7123_reset(Device* device) { + auto* internal = static_cast(device_get_driver_data(device)); + return esp_lcd_panel_reset(internal->panel_handle) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE; +} + +static error_t st7123_init(Device* device) { + auto* internal = static_cast(device_get_driver_data(device)); + return esp_lcd_panel_init(internal->panel_handle) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE; +} + +// Only block for scan-out completion when color_data is actually one of the panel's own frame +// buffers (see on_refresh_done's comment above for why that matters) and allow_tearing is not +// set - i.e. this specific call is a zero-copy flip, not a plain CPU copy into the panel's buffer +// from a caller-owned one (e.g. LVGL bound in owned-buffer mode), which has no reuse race to +// guard against and shouldn't pay the up-to-one-frame latency cost for every partial update. +static bool st7123_color_data_is_frame_buffer(const St7123Internal* internal, const void* color_data) { + const auto* ptr = static_cast(color_data); + for (uint8_t i = 0; i < internal->frame_buffer_count; i++) { + const auto* base = static_cast(internal->frame_buffers[i]); + if (ptr >= base && ptr < base + internal->frame_buffer_size_bytes) { + return true; + } + } + return false; +} + +static error_t st7123_draw_bitmap(Device* device, int32_t x_start, int32_t y_start, int32_t x_end, int32_t y_end, const void* color_data) { + auto* internal = static_cast(device_get_driver_data(device)); + + bool wait_for_scanout = !GET_CONFIG(device)->allow_tearing && st7123_color_data_is_frame_buffer(internal, color_data); + if (wait_for_scanout) { + xSemaphoreTake(internal->frame_complete_semaphore, 0); // clear any already-pending signal + } + + if (esp_lcd_panel_draw_bitmap(internal->panel_handle, x_start, y_start, x_end, y_end, color_data) != ESP_OK) { + return ERROR_RESOURCE; + } + + if (wait_for_scanout) { + xSemaphoreTake(internal->frame_complete_semaphore, portMAX_DELAY); + } + + return ERROR_NONE; +} + +// Mirror is always implemented by LCD command (MADCTL), unconditionally, by esp_lcd_st7123 - see +// panel_st7123_mirror() in esp_lcd_st7123.c. Unlike esp_lcd_rgb_panel's software rotate_mask +// trick, this isn't tied to draw_bitmap's copy path, so it stays available even when LVGL is +// bound directly onto the panel's own frame buffers. +static error_t st7123_mirror(Device* device, bool x_axis, bool y_axis) { + auto* internal = static_cast(device_get_driver_data(device)); + return esp_lcd_panel_mirror(internal->panel_handle, x_axis, y_axis) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE; +} + +static bool st7123_get_mirror_x(Device* device) { + return GET_CONFIG(device)->mirror_x; +} + +static bool st7123_get_mirror_y(Device* device) { + return GET_CONFIG(device)->mirror_y; +} + +// swap_xy/set_gap are not exposed: esp_lcd_st7123 doesn't override them and the underlying raw +// MIPI DPI panel doesn't implement them either (esp_lcd_panel_dpi.c never assigns those function +// pointers), so esp_lcd_panel_swap_xy()/set_gap() would just return ESP_ERR_NOT_SUPPORTED. + +static error_t st7123_invert_color(Device* device, bool invert_color_data) { + auto* internal = static_cast(device_get_driver_data(device)); + return esp_lcd_panel_invert_color(internal->panel_handle, invert_color_data) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE; +} + +static error_t st7123_disp_on_off(Device* device, bool on_off) { + auto* internal = static_cast(device_get_driver_data(device)); + return esp_lcd_panel_disp_on_off(internal->panel_handle, on_off) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE; +} + +// disp_sleep is not exposed: esp_lcd_st7123 doesn't override it either (only +// del/init/reset/mirror/invert_color/disp_on_off - see esp_lcd_st7123.c). + +// bgr_order only selects the panel controller's rgb_ele_order (applied in start(), above) so the +// R/B swap happens on-chip. LVGL always fills the same-layout buffer either way - there's no +// separate "BGR" memory layout to produce, unlike the SPI byte-order swap some other panels need. +static enum DisplayColorFormat st7123_get_color_format(Device* device) { + return GET_CONFIG(device)->bits_per_pixel == 24 ? DISPLAY_COLOR_FORMAT_RGB888 : DISPLAY_COLOR_FORMAT_RGB565; +} + +static uint16_t st7123_get_resolution_x(Device* device) { + return GET_CONFIG(device)->horizontal_resolution; +} + +static uint16_t st7123_get_resolution_y(Device* device) { + return GET_CONFIG(device)->vertical_resolution; +} + +static void st7123_get_frame_buffer(Device* device, uint8_t index, void** out_buffer) { + auto* internal = static_cast(device_get_driver_data(device)); + *out_buffer = index < internal->frame_buffer_count ? internal->frame_buffers[index] : nullptr; +} + +static uint8_t st7123_get_frame_buffer_count(Device* device) { + auto* internal = static_cast(device_get_driver_data(device)); + return internal->frame_buffer_count; +} + +static error_t st7123_get_backlight(Device* device, Device** backlight) { + auto* configured_backlight = GET_CONFIG(device)->backlight; + if (configured_backlight == nullptr) { + return ERROR_NOT_SUPPORTED; + } + *backlight = configured_backlight; + return ERROR_NONE; +} + +// endregion + +static const DisplayApi st7123_display_api = { + .capabilities = DISPLAY_CAPABILITY_CAP_MIRROR | DISPLAY_CAPABILITY_INVERT_COLOR | + DISPLAY_CAPABILITY_ON_OFF | DISPLAY_CAPABILITY_BACKLIGHT, + .reset = st7123_reset, + .init = st7123_init, + .draw_bitmap = st7123_draw_bitmap, + .mirror = st7123_mirror, + .swap_xy = nullptr, + .get_swap_xy = nullptr, + .get_mirror_x = st7123_get_mirror_x, + .get_mirror_y = st7123_get_mirror_y, + .set_gap = nullptr, + .invert_color = st7123_invert_color, + .disp_on_off = st7123_disp_on_off, + .disp_sleep = nullptr, + .get_color_format = st7123_get_color_format, + .get_resolution_x = st7123_get_resolution_x, + .get_resolution_y = st7123_get_resolution_y, + .get_frame_buffer = st7123_get_frame_buffer, + .get_frame_buffer_count = st7123_get_frame_buffer_count, + .get_backlight = st7123_get_backlight, + .has_capability = nullptr, +}; + +Driver st7123_driver = { + .name = "st7123", + .compatible = (const char*[]) { "sitronix,st7123", nullptr }, + .start_device = start, + .stop_device = stop, + .api = &st7123_display_api, + .device_type = &DISPLAY_TYPE, + .owner = &st7123_module, + .internal = nullptr +}; + +#endif // SOC_MIPI_DSI_SUPPORTED diff --git a/Drivers/st7123-module/source/st7123_touch.cpp b/Drivers/st7123-module/source/st7123_touch.cpp new file mode 100644 index 000000000..668179d70 --- /dev/null +++ b/Drivers/st7123-module/source/st7123_touch.cpp @@ -0,0 +1,210 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +#define TAG "St7123Touch" +#define GET_CONFIG(device) (static_cast((device)->config)) + +struct St7123TouchInternal { + esp_lcd_panel_io_handle_t io_handle; + esp_lcd_touch_handle_t touch_handle; +}; + +static gpio_num_t pin_or_nc(const GpioPinSpec& pin) { + return pin.gpio_controller == nullptr ? GPIO_NUM_NC : static_cast(pin.pin); +} + +// region Driver lifecycle + +// Unlike GT911, ST7123's I2C address is fixed (see ESP_LCD_TOUCH_IO_I2C_ST7123_ADDRESS) - no +// probing needed, the devicetree "reg" hint is purely documentation here. +static esp_err_t create_io_handle(Device* parent, esp_lcd_panel_io_handle_t* out_handle) { + esp_lcd_panel_io_i2c_config_t io_config = ESP_LCD_TOUCH_IO_I2C_ST7123_CONFIG(); + + auto* parent_driver = device_get_driver(parent); + if (driver_is_compatible(parent_driver, "espressif,esp32-i2c")) { + auto port = static_cast(parent->config)->port; + return esp_lcd_new_panel_io_i2c_v1(port, &io_config, out_handle); + } + if (driver_is_compatible(parent_driver, "espressif,esp32-i2c-master")) { + auto bus = esp32_i2c_master_get_bus_handle(parent); + io_config.scl_speed_hz = esp32_i2c_master_get_clock_frequency(parent); + return esp_lcd_new_panel_io_i2c_v2(bus, &io_config, out_handle); + } + + LOG_E(TAG, "Unsupported I2C driver"); + return ESP_ERR_NOT_SUPPORTED; +} + +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(malloc(sizeof(St7123TouchInternal))); + if (internal == nullptr) { + return ERROR_OUT_OF_MEMORY; + } + + esp_err_t ret = create_io_handle(parent, &internal->io_handle); + if (ret != ESP_OK) { + free(internal); + return ERROR_RESOURCE; + } + + esp_lcd_touch_config_t touch_config = { + .x_max = config->x_max, + .y_max = config->y_max, + .rst_gpio_num = pin_or_nc(config->pin_reset), + .int_gpio_num = pin_or_nc(config->pin_interrupt), + .levels = { + .reset = config->reset_active_high ? 1u : 0u, + .interrupt = config->interrupt_active_high ? 1u : 0u, + }, + .flags = { + .swap_xy = config->swap_xy ? 1u : 0u, + .mirror_x = config->mirror_x ? 1u : 0u, + .mirror_y = config->mirror_y ? 1u : 0u, + }, + .process_coordinates = nullptr, + .interrupt_callback = nullptr, + .user_data = nullptr, + .driver_data = nullptr, + }; + + ret = esp_lcd_touch_new_i2c_st7123(internal->io_handle, &touch_config, &internal->touch_handle); + if (ret != ESP_OK) { + LOG_E(TAG, "Failed to create touch handle: %s", esp_err_to_name(ret)); + esp_lcd_panel_io_del(internal->io_handle); + free(internal); + return ERROR_RESOURCE; + } + + device_set_driver_data(device, internal); + return ERROR_NONE; +} + +static error_t stop(Device* device) { + auto* internal = static_cast(device_get_driver_data(device)); + + // esp_lcd_touch_del() only releases the touch-side resources; the panel IO handle is owned + // separately and needs its own deletion. + if (internal->touch_handle != nullptr) { + if (esp_lcd_touch_del(internal->touch_handle) != ESP_OK) { + LOG_E(TAG, "Failed to delete touch handle"); + return ERROR_RESOURCE; + } + internal->touch_handle = nullptr; + } + + if (internal->io_handle != nullptr) { + if (esp_lcd_panel_io_del(internal->io_handle) != ESP_OK) { + LOG_E(TAG, "Failed to delete panel IO handle"); + return ERROR_RESOURCE; + } + internal->io_handle = nullptr; + } + + free(internal); + device_set_driver_data(device, nullptr); + return ERROR_NONE; +} + +// endregion + +// region PointerApi + +static error_t st7123_touch_enter_sleep(Device* device) { + auto* internal = static_cast(device_get_driver_data(device)); + return esp_lcd_touch_enter_sleep(internal->touch_handle) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE; +} + +static error_t st7123_touch_exit_sleep(Device* device) { + auto* internal = static_cast(device_get_driver_data(device)); + return esp_lcd_touch_exit_sleep(internal->touch_handle) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE; +} + +static error_t st7123_touch_read_data(Device* device, TickType_t timeout) { + (void)timeout; // esp_lcd_touch_read_data() has no timeout parameter + auto* internal = static_cast(device_get_driver_data(device)); + return esp_lcd_touch_read_data(internal->touch_handle) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE; +} + +static bool st7123_touch_get_touched_points(Device* device, uint16_t* x, uint16_t* y, uint16_t* strength, uint8_t* point_count, uint8_t max_point_count) { + auto* internal = static_cast(device_get_driver_data(device)); + return esp_lcd_touch_get_coordinates(internal->touch_handle, x, y, strength, point_count, max_point_count); +} + +static error_t st7123_touch_set_swap_xy(Device* device, bool swap) { + auto* internal = static_cast(device_get_driver_data(device)); + return esp_lcd_touch_set_swap_xy(internal->touch_handle, swap) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE; +} + +static error_t st7123_touch_get_swap_xy(Device* device, bool* swap) { + auto* internal = static_cast(device_get_driver_data(device)); + return esp_lcd_touch_get_swap_xy(internal->touch_handle, swap) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE; +} + +static error_t st7123_touch_set_mirror_x(Device* device, bool mirror) { + auto* internal = static_cast(device_get_driver_data(device)); + return esp_lcd_touch_set_mirror_x(internal->touch_handle, mirror) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE; +} + +static error_t st7123_touch_get_mirror_x(Device* device, bool* mirror) { + auto* internal = static_cast(device_get_driver_data(device)); + return esp_lcd_touch_get_mirror_x(internal->touch_handle, mirror) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE; +} + +static error_t st7123_touch_set_mirror_y(Device* device, bool mirror) { + auto* internal = static_cast(device_get_driver_data(device)); + return esp_lcd_touch_set_mirror_y(internal->touch_handle, mirror) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE; +} + +static error_t st7123_touch_get_mirror_y(Device* device, bool* mirror) { + auto* internal = static_cast(device_get_driver_data(device)); + return esp_lcd_touch_get_mirror_y(internal->touch_handle, mirror) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE; +} + +// endregion + +static const PointerApi st7123_touch_pointer_api = { + .enter_sleep = st7123_touch_enter_sleep, + .exit_sleep = st7123_touch_exit_sleep, + .read_data = st7123_touch_read_data, + .get_touched_points = st7123_touch_get_touched_points, + .set_swap_xy = st7123_touch_set_swap_xy, + .get_swap_xy = st7123_touch_get_swap_xy, + .set_mirror_x = st7123_touch_set_mirror_x, + .get_mirror_x = st7123_touch_get_mirror_x, + .set_mirror_y = st7123_touch_set_mirror_y, + .get_mirror_y = st7123_touch_get_mirror_y, +}; + +Driver st7123_touch_driver = { + .name = "st7123-touch", + .compatible = (const char*[]) { "sitronix,st7123-touch", nullptr }, + .start_device = start, + .stop_device = stop, + .api = &st7123_touch_pointer_api, + .device_type = &POINTER_TYPE, + .owner = &st7123_module, + .internal = nullptr +}; diff --git a/Drivers/st7796-i8080-module/CMakeLists.txt b/Drivers/st7796-i8080-module/CMakeLists.txt new file mode 100644 index 000000000..0ffd4a443 --- /dev/null +++ b/Drivers/st7796-i8080-module/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.20) + +include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake") + +file(GLOB_RECURSE SOURCE_FILES "source/*.c*") + +tactility_add_module(st7796-i8080-module + SRCS ${SOURCE_FILES} + INCLUDE_DIRS include/ + REQUIRES TactilityKernel platform-esp32 esp_lcd_st7796 driver +) diff --git a/Drivers/st7796-i8080-module/LICENSE-Apache-2.0.md b/Drivers/st7796-i8080-module/LICENSE-Apache-2.0.md new file mode 100644 index 000000000..f5f4b8b5e --- /dev/null +++ b/Drivers/st7796-i8080-module/LICENSE-Apache-2.0.md @@ -0,0 +1,195 @@ +Apache License +============== + +_Version 2.0, January 2004_ +_<>_ + +### Terms and Conditions for use, reproduction, and distribution + +#### 1. Definitions + +“License” shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +“Licensor” shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +“Legal Entity” shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, “control” means **(i)** the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the +outstanding shares, or **(iii)** beneficial ownership of such entity. + +“You” (or “Your”) shall mean an individual or Legal Entity exercising +permissions granted by this License. + +“Source” form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +“Object” form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +“Work” shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +“Derivative Works” shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +“Contribution” shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +“submitted” means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as “Not a Contribution.” + +“Contributor” shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +#### 2. Grant of Copyright License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +#### 3. Grant of Patent License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +#### 4. Redistribution + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +* **(a)** You must give any other recipients of the Work or Derivative Works a copy of +this License; and +* **(b)** You must cause any modified files to carry prominent notices stating that You +changed the files; and +* **(c)** You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. + +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +#### 5. Submission of Contributions + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +#### 6. Trademarks + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +#### 7. Disclaimer of Warranty + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an “AS IS” BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +#### 8. Limitation of Liability + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +#### 9. Accepting Warranty or Additional Liability + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +_END OF TERMS AND CONDITIONS_ + +### APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets `[]` replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same “printed page” as the copyright notice for easier identification within +third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/Drivers/st7796-i8080-module/README.md b/Drivers/st7796-i8080-module/README.md new file mode 100644 index 000000000..45b5489b5 --- /dev/null +++ b/Drivers/st7796-i8080-module/README.md @@ -0,0 +1,6 @@ +# ST7796 (i8080) display + +A driver for the `ST7796` display panel by Sitronix, driven over an i8080 (8080-series +parallel) bus. See [ST7796 (SPI)](../st7796-module) for the SPI variant. + +License: [Apache v2.0](LICENSE-Apache-2.0.md) diff --git a/Drivers/st7796-i8080-module/bindings/sitronix,st7796-i8080.yaml b/Drivers/st7796-i8080-module/bindings/sitronix,st7796-i8080.yaml new file mode 100644 index 000000000..0159d332e --- /dev/null +++ b/Drivers/st7796-i8080-module/bindings/sitronix,st7796-i8080.yaml @@ -0,0 +1,69 @@ +description: Sitronix ST7796 display panel over an i8080 (parallel) bus + +compatible: "sitronix,st7796-i8080" + +bus: i8080 + +properties: + horizontal-resolution: + type: int + required: true + description: Horizontal resolution in pixels + vertical-resolution: + type: int + required: true + description: Vertical resolution in pixels + gap-x: + type: int + default: 0 + description: X offset applied to all draw operations + gap-y: + type: int + default: 0 + description: Y offset applied to all draw operations + swap-xy: + type: boolean + default: false + description: Swap the X and Y axes + mirror-x: + type: boolean + default: false + description: Mirror the X axis + mirror-y: + type: boolean + default: false + description: Mirror the Y axis + invert-color: + type: boolean + default: false + description: Invert the panel's color output + bgr-order: + type: boolean + default: false + description: Use BGR element order instead of RGB + pixel-clock-hz: + type: int + default: 16000000 + description: i80 bus write-clock frequency in Hz + transaction-queue-depth: + type: int + default: 10 + description: Size of the internal transaction queue + gamma-curve: + type: int + default: 1 + min: 0 + max: 3 + description: Gamma curve preset index [0,3], sent via the MIPI DCS GAMSET (0x26) command at bring-up + pin-reset: + type: phandles + default: GPIO_PIN_SPEC_NONE + description: Reset GPIO pin + reset-active-high: + type: boolean + default: false + description: Whether the reset pin is active high + backlight: + type: phandle + default: "NULL" + description: Optional reference to this display's backlight device diff --git a/Drivers/st7796-i8080-module/devicetree.yaml b/Drivers/st7796-i8080-module/devicetree.yaml new file mode 100644 index 000000000..a07d6f334 --- /dev/null +++ b/Drivers/st7796-i8080-module/devicetree.yaml @@ -0,0 +1,3 @@ +dependencies: + - TactilityKernel +bindings: bindings diff --git a/Drivers/st7796-i8080-module/include/bindings/st7796_i8080.h b/Drivers/st7796-i8080-module/include/bindings/st7796_i8080.h new file mode 100644 index 000000000..cd3cd51d8 --- /dev/null +++ b/Drivers/st7796-i8080-module/include/bindings/st7796_i8080.h @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include + +DEFINE_DEVICETREE(st7796_i8080, struct St7796I8080Config) diff --git a/Drivers/st7796-i8080-module/include/drivers/st7796_i8080.h b/Drivers/st7796-i8080-module/include/drivers/st7796_i8080.h new file mode 100644 index 000000000..f99fe2a22 --- /dev/null +++ b/Drivers/st7796-i8080-module/include/drivers/st7796_i8080.h @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include +#include + +struct St7796I8080Config { + uint16_t horizontal_resolution; + uint16_t vertical_resolution; + int32_t gap_x; + int32_t gap_y; + bool swap_xy; + bool mirror_x; + bool mirror_y; + bool invert_color; + bool bgr_order; + uint32_t pixel_clock_hz; + uint8_t transaction_queue_depth; + // Gamma curve preset index [0,3], sent via the MIPI DCS GAMSET (0x26) command at bring-up. + uint8_t gamma_curve; + struct GpioPinSpec pin_reset; + bool reset_active_high; + // Optional reference to this display's backlight device, NULL if none. + struct Device* backlight; +}; + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/st7796-i8080-module/include/st7796_i8080_module.h b/Drivers/st7796-i8080-module/include/st7796_i8080_module.h new file mode 100644 index 000000000..607e45990 --- /dev/null +++ b/Drivers/st7796-i8080-module/include/st7796_i8080_module.h @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +extern struct Module st7796_i8080_module; + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/st7796-i8080-module/source/module.cpp b/Drivers/st7796-i8080-module/source/module.cpp new file mode 100644 index 000000000..037c4c28a --- /dev/null +++ b/Drivers/st7796-i8080-module/source/module.cpp @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include + +extern "C" { + +extern Driver st7796_i8080_driver; + +static error_t start() { + /* We crash when construct fails, because if a single driver fails to construct, + * there is no guarantee that the previously constructed drivers can be destroyed */ + check(driver_construct_add(&st7796_i8080_driver) == ERROR_NONE); + return ERROR_NONE; +} + +static error_t stop() { + /* We crash when destruct fails, because if a single driver fails to destruct, + * there is no guarantee that the previously destroyed drivers can be recovered */ + check(driver_remove_destruct(&st7796_i8080_driver) == ERROR_NONE); + return ERROR_NONE; +} + +Module st7796_i8080_module = { + .name = "st7796_i8080", + .start = start, + .stop = stop, + .symbols = nullptr, + .internal = nullptr +}; + +} // extern "C" diff --git a/Drivers/st7796-i8080-module/source/st7796_i8080.cpp b/Drivers/st7796-i8080-module/source/st7796_i8080.cpp new file mode 100644 index 000000000..e5aa9cd40 --- /dev/null +++ b/Drivers/st7796-i8080-module/source/st7796_i8080.cpp @@ -0,0 +1,338 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include + +#include + +constexpr auto* TAG = "ST7796I8080"; +#define GET_CONFIG(device) (static_cast((device)->config)) + +// Maps gamma-curve devicetree index [0,3] to the MIPI DCS GAMSET (0x26) parameter value. Mirrors +// the deprecated HAL's St7796Display::setGammaCurve() (Drivers/ST7796) - note the non-linear +// mapping, not index+1. +static const uint8_t GAMMA_CURVE_VALUES[4] = { 0x01, 0x04, 0x02, 0x08 }; + +struct St7796I8080Internal { + esp_lcd_panel_io_handle_t io_handle; + esp_lcd_panel_handle_t panel_handle; + // See ili9341-module's identical field for why this exists: draw_bitmap() must block until + // the transfer physically completes (DisplayApi's synchronous contract). + SemaphoreHandle_t draw_done_semaphore; +}; + +static bool IRAM_ATTR on_color_trans_done(esp_lcd_panel_io_handle_t, esp_lcd_panel_io_event_data_t*, void* user_ctx) { + auto* internal = static_cast(user_ctx); + BaseType_t high_task_woken = pdFALSE; + xSemaphoreGiveFromISR(internal->draw_done_semaphore, &high_task_woken); + return high_task_woken == pdTRUE; +} + +static int pin_or_unused(const struct GpioPinSpec& pin) { + return pin.gpio_controller == nullptr ? -1 : static_cast(pin.pin); +} + +// region Driver lifecycle + +static error_t start(Device* device) { + auto* parent = device_get_parent(device); + check(device_get_type(parent) == &I8080_CONTROLLER_TYPE); + + esp_lcd_i80_bus_handle_t bus_handle; + if (esp32_i8080_get_bus_handle(device, &bus_handle) != ERROR_NONE) { + LOG_E(TAG, "Failed to resolve i80 bus handle"); + return ERROR_RESOURCE; + } + + struct GpioPinSpec cs_pin; + if (esp32_i8080_get_cs_pin(device, &cs_pin) != ERROR_NONE) { + LOG_E(TAG, "Failed to resolve CS pin"); + return ERROR_RESOURCE; + } + + const auto* config = GET_CONFIG(device); + + auto* internal = static_cast(malloc(sizeof(St7796I8080Internal))); + if (internal == nullptr) { + return ERROR_OUT_OF_MEMORY; + } + + internal->draw_done_semaphore = xSemaphoreCreateBinary(); + if (internal->draw_done_semaphore == nullptr) { + free(internal); + return ERROR_OUT_OF_MEMORY; + } + + esp_lcd_panel_io_i80_config_t io_config = { + .cs_gpio_num = pin_or_unused(cs_pin), + .pclk_hz = config->pixel_clock_hz, + .trans_queue_depth = config->transaction_queue_depth, + .on_color_trans_done = on_color_trans_done, + .user_ctx = internal, + .lcd_cmd_bits = 8, + .lcd_param_bits = 8, + .dc_levels = { + .dc_idle_level = 0, + .dc_cmd_level = 0, + .dc_dummy_level = 0, + .dc_data_level = 1, + }, + .flags = { + .cs_active_high = 0, + .reverse_color_bits = 0, + .swap_color_bytes = 1, + .pclk_active_neg = 0, + .pclk_idle_low = 0, + }, + }; + + esp_err_t ret = esp_lcd_new_panel_io_i80(bus_handle, &io_config, &internal->io_handle); + if (ret != ESP_OK) { + LOG_E(TAG, "Failed to create panel IO: %s", esp_err_to_name(ret)); + vSemaphoreDelete(internal->draw_done_semaphore); + free(internal); + return ERROR_RESOURCE; + } + + esp_lcd_panel_dev_config_t panel_config = { + .reset_gpio_num = pin_or_unused(config->pin_reset), + .rgb_ele_order = config->bgr_order ? LCD_RGB_ELEMENT_ORDER_BGR : LCD_RGB_ELEMENT_ORDER_RGB, + .data_endian = LCD_RGB_DATA_ENDIAN_LITTLE, + .bits_per_pixel = 16, + .flags = { .reset_active_high = config->reset_active_high }, + .vendor_config = nullptr, + }; + + ret = esp_lcd_new_panel_st7796(internal->io_handle, &panel_config, &internal->panel_handle); + if (ret != ESP_OK) { + LOG_E(TAG, "Failed to create panel: %s", esp_err_to_name(ret)); + esp_lcd_panel_io_del(internal->io_handle); + vSemaphoreDelete(internal->draw_done_semaphore); + free(internal); + return ERROR_RESOURCE; + } + + // Bring-up sequence, order matches the deprecated HAL's St7796i8080Display (proven correct on + // real WT32-SC01 Plus hardware). Every failure path below must clean up fully, same reasoning + // as ili9341-module's start(). + bool ok = + esp_lcd_panel_reset(internal->panel_handle) == ESP_OK && + esp_lcd_panel_init(internal->panel_handle) == ESP_OK && + (!config->invert_color || esp_lcd_panel_invert_color(internal->panel_handle, true) == ESP_OK); + + // set_gap() just stores x_gap/y_gap and adds them as raw offsets wherever draw_bitmap()'s + // (already logical, post-swap) x/y land - independent of swap_xy/mirror state, so gap_x/gap_y + // are passed through unswapped (matches st7796-module, the SPI sibling of this driver). + if (ok) { + ok = (config->gap_x == 0 && config->gap_y == 0) || esp_lcd_panel_set_gap(internal->panel_handle, config->gap_x, config->gap_y) == ESP_OK; + } + ok = ok && (!config->swap_xy || esp_lcd_panel_swap_xy(internal->panel_handle, true) == ESP_OK); + ok = ok && ((!config->mirror_x && !config->mirror_y) || esp_lcd_panel_mirror(internal->panel_handle, config->mirror_x, config->mirror_y) == ESP_OK); + ok = ok && (!config->invert_color || esp_lcd_panel_invert_color(internal->panel_handle, true) == ESP_OK); + ok = ok && (config->gamma_curve >= 4 || esp_lcd_panel_io_tx_param(internal->io_handle, LCD_CMD_GAMSET, &GAMMA_CURVE_VALUES[config->gamma_curve], 1) == ESP_OK); + ok = ok && esp_lcd_panel_disp_on_off(internal->panel_handle, true) == ESP_OK; + + if (!ok) { + LOG_E(TAG, "Failed to bring up panel"); + esp_lcd_panel_del(internal->panel_handle); + esp_lcd_panel_io_del(internal->io_handle); + vSemaphoreDelete(internal->draw_done_semaphore); + free(internal); + return ERROR_RESOURCE; + } + + device_set_driver_data(device, internal); + return ERROR_NONE; +} + +static error_t stop(Device* device) { + auto* internal = static_cast(device_get_driver_data(device)); + + if (internal->panel_handle != nullptr) { + if (esp_lcd_panel_del(internal->panel_handle) != ESP_OK) { + LOG_E(TAG, "Failed to delete panel"); + return ERROR_RESOURCE; + } + internal->panel_handle = nullptr; + } + + if (internal->io_handle != nullptr) { + if (esp_lcd_panel_io_del(internal->io_handle) != ESP_OK) { + LOG_E(TAG, "Failed to delete panel IO"); + return ERROR_RESOURCE; + } + internal->io_handle = nullptr; + } + + vSemaphoreDelete(internal->draw_done_semaphore); + free(internal); + device_set_driver_data(device, nullptr); + return ERROR_NONE; +} + +// endregion + +// region DisplayApi + +static error_t st7796_i8080_reset(Device* device) { + auto* internal = static_cast(device_get_driver_data(device)); + return esp_lcd_panel_reset(internal->panel_handle) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE; +} + +static error_t st7796_i8080_init(Device* device) { + auto* internal = static_cast(device_get_driver_data(device)); + return esp_lcd_panel_init(internal->panel_handle) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE; +} + +static error_t st7796_i8080_draw_bitmap(Device* device, int32_t x_start, int32_t y_start, int32_t x_end, int32_t y_end, const void* color_data) { + auto* internal = static_cast(device_get_driver_data(device)); + + xSemaphoreTake(internal->draw_done_semaphore, 0); + + if (esp_lcd_panel_draw_bitmap(internal->panel_handle, x_start, y_start, x_end, y_end, color_data) != ESP_OK) { + return ERROR_RESOURCE; + } + + xSemaphoreTake(internal->draw_done_semaphore, portMAX_DELAY); + return ERROR_NONE; +} + +static error_t st7796_i8080_mirror(Device* device, bool x_axis, bool y_axis) { + auto* internal = static_cast(device_get_driver_data(device)); + return esp_lcd_panel_mirror(internal->panel_handle, x_axis, y_axis) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE; +} + +static error_t st7796_i8080_swap_xy(Device* device, bool swap_axes) { + auto* internal = static_cast(device_get_driver_data(device)); + return esp_lcd_panel_swap_xy(internal->panel_handle, swap_axes) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE; +} + +// Reads the devicetree-configured baseline, not live hardware state - see st7796-module (the SPI +// sibling of this driver) for the full reasoning. +static bool st7796_i8080_get_swap_xy(Device* device) { + return GET_CONFIG(device)->swap_xy; +} + +static bool st7796_i8080_get_mirror_x(Device* device) { + return GET_CONFIG(device)->mirror_x; +} + +static bool st7796_i8080_get_mirror_y(Device* device) { + return GET_CONFIG(device)->mirror_y; +} + +static error_t st7796_i8080_set_gap(Device* device, int32_t x_gap, int32_t y_gap) { + auto* internal = static_cast(device_get_driver_data(device)); + return esp_lcd_panel_set_gap(internal->panel_handle, x_gap, y_gap) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE; +} + +static int32_t st7796_i8080_get_gap_x(Device* device) { + return GET_CONFIG(device)->gap_x; +} + +static int32_t st7796_i8080_get_gap_y(Device* device) { + return GET_CONFIG(device)->gap_y; +} + +static error_t st7796_i8080_invert_color(Device* device, bool invert_color_data) { + auto* internal = static_cast(device_get_driver_data(device)); + return esp_lcd_panel_invert_color(internal->panel_handle, invert_color_data) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE; +} + +static error_t st7796_i8080_disp_on_off(Device* device, bool on_off) { + auto* internal = static_cast(device_get_driver_data(device)); + return esp_lcd_panel_disp_on_off(internal->panel_handle, on_off) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE; +} + +static error_t st7796_i8080_disp_sleep(Device* device, bool sleep) { + auto* internal = static_cast(device_get_driver_data(device)); + return esp_lcd_panel_disp_sleep(internal->panel_handle, sleep) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE; +} + +// swap_color_bytes=1 in the panel IO config means the i80 peripheral itself byte-swaps each +// RGB565 pixel over the parallel bus - same reasoning as st7789-i8080-module's identical knob. +static enum DisplayColorFormat st7796_i8080_get_color_format(Device*) { + return DISPLAY_COLOR_FORMAT_RGB565_SWAPPED; +} + +static uint16_t st7796_i8080_get_resolution_x(Device* device) { + return GET_CONFIG(device)->horizontal_resolution; +} + +static uint16_t st7796_i8080_get_resolution_y(Device* device) { + return GET_CONFIG(device)->vertical_resolution; +} + +static void st7796_i8080_get_frame_buffer(Device*, uint8_t, void** out_buffer) { + *out_buffer = nullptr; +} + +static uint8_t st7796_i8080_get_frame_buffer_count(Device*) { + return 0; +} + +static error_t st7796_i8080_get_backlight(Device* device, Device** backlight) { + auto* configured_backlight = GET_CONFIG(device)->backlight; + if (configured_backlight == nullptr) { + return ERROR_NOT_SUPPORTED; + } + *backlight = configured_backlight; + return ERROR_NONE; +} + +// endregion + +static const DisplayApi st7796_i8080_display_api = { + .capabilities = DISPLAY_CAPABILITY_CAP_MIRROR | DISPLAY_CAPABILITY_CAP_SWAP_XY | + DISPLAY_CAPABILITY_CAP_SET_GAP | DISPLAY_CAPABILITY_INVERT_COLOR | DISPLAY_CAPABILITY_ON_OFF | + DISPLAY_CAPABILITY_SLEEP | DISPLAY_CAPABILITY_BACKLIGHT, + .reset = st7796_i8080_reset, + .init = st7796_i8080_init, + .draw_bitmap = st7796_i8080_draw_bitmap, + .mirror = st7796_i8080_mirror, + .swap_xy = st7796_i8080_swap_xy, + .get_swap_xy = st7796_i8080_get_swap_xy, + .get_mirror_x = st7796_i8080_get_mirror_x, + .get_mirror_y = st7796_i8080_get_mirror_y, + .set_gap = st7796_i8080_set_gap, + .get_gap_x = st7796_i8080_get_gap_x, + .get_gap_y = st7796_i8080_get_gap_y, + .invert_color = st7796_i8080_invert_color, + .disp_on_off = st7796_i8080_disp_on_off, + .disp_sleep = st7796_i8080_disp_sleep, + .get_color_format = st7796_i8080_get_color_format, + .get_resolution_x = st7796_i8080_get_resolution_x, + .get_resolution_y = st7796_i8080_get_resolution_y, + .get_frame_buffer = st7796_i8080_get_frame_buffer, + .get_frame_buffer_count = st7796_i8080_get_frame_buffer_count, + .get_backlight = st7796_i8080_get_backlight, + .has_capability = nullptr, +}; + +Driver st7796_i8080_driver = { + .name = "st7796_i8080", + .compatible = (const char*[]) { "sitronix,st7796-i8080", nullptr }, + .start_device = start, + .stop_device = stop, + .api = &st7796_i8080_display_api, + .device_type = &DISPLAY_TYPE, + .owner = &st7796_i8080_module, + .internal = nullptr +}; diff --git a/Modules/crypt-module/source/module.cpp b/Modules/crypt-module/source/module.cpp index 25501b3f7..d1e413499 100644 --- a/Modules/crypt-module/source/module.cpp +++ b/Modules/crypt-module/source/module.cpp @@ -1,18 +1,11 @@ // SPDX-License-Identifier: Apache-2.0 #include #include +#include #include extern "C" { -static error_t start() { - return ERROR_NONE; -} - -static error_t stop() { - return ERROR_NONE; -} - static const ModuleSymbol crypt_module_symbols[] = { DEFINE_MODULE_SYMBOL(crypt_get_iv), DEFINE_MODULE_SYMBOL(crypt_generate_iv), @@ -25,8 +18,8 @@ static const ModuleSymbol crypt_module_symbols[] = { Module crypt_module = { .name = "crypt", - .start = start, - .stop = stop, + .start = [] -> error_t { return ERROR_NONE; }, + .stop = [] -> error_t { return ERROR_NONE; }, .symbols = crypt_module_symbols, .internal = nullptr }; diff --git a/Modules/lvgl-module/source/lvgl_devices.c b/Modules/lvgl-module/source/lvgl_devices.c index 69150527c..cbd61a593 100644 --- a/Modules/lvgl-module/source/lvgl_devices.c +++ b/Modules/lvgl-module/source/lvgl_devices.c @@ -35,8 +35,17 @@ void lvgl_devices_attach() { color_format == DISPLAY_COLOR_FORMAT_BGR565; bool display_requires_full_frame = display_has_capability(kernel_display_device, DISPLAY_CAPABILITY_REQUIRES_FULL_FRAME); display_updates_slowly = display_has_capability(kernel_display_device, DISPLAY_CAPABILITY_SLOW_REFRESH); + // Without CAP_SWAP_XY the driver can't rotate 90/270 in hardware (display_swap_xy() is + // null and silently skipped by lvgl_display_apply_rotation()) - LVGL would still switch + // its own logical w/h for those rotations, mismatching the panel's fixed physical + // orientation (e.g. RGB/DPI panels, whose video timing is fixed at panel-init time). + // sw_rotate makes LVGL rotate the rendered pixels in software instead, so the driver + // itself is never asked to do something it can't. + bool can_hw_rotate = display_has_capability(kernel_display_device, DISPLAY_CAPABILITY_CAP_SWAP_XY) && + display_has_capability(kernel_display_device, DISPLAY_CAPABILITY_CAP_MIRROR); struct LvglDisplayConfig lvgl_display_config = { .buffer_height = vres > 10 ? vres / 10 : vres, + .sw_rotate = !can_hw_rotate, .swap_bytes = swap_bytes, .force_full_frame = display_requires_full_frame }; diff --git a/Modules/lvgl-module/source/lvgl_display.c b/Modules/lvgl-module/source/lvgl_display.c index 48266ae91..a7c535ad0 100644 --- a/Modules/lvgl-module/source/lvgl_display.c +++ b/Modules/lvgl-module/source/lvgl_display.c @@ -51,9 +51,13 @@ struct LvglDisplayCtx { static void* lvgl_display_alloc_buffer(size_t size_bytes) { #ifdef ESP_PLATFORM - void* buf = heap_caps_aligned_alloc(4, size_bytes, MALLOC_CAP_DMA | MALLOC_CAP_8BIT); + // Must match LV_DRAW_BUF_ALIGN (can be > 4 - e.g. 64, tied to the cache line size for + // DMA2D/PPA coherency on some targets - see sdkconfig's CONFIG_LV_DRAW_BUF_ALIGN). A buffer + // allocated less strictly than that fails lv_display_set_buffers()'s alignment assert, which + // is configured to LV_ASSERT_HANDLER (while(1);) rather than a clean abort - i.e. a silent hang. + void* buf = heap_caps_aligned_alloc(LV_DRAW_BUF_ALIGN, size_bytes, MALLOC_CAP_DMA | MALLOC_CAP_8BIT); if (buf == NULL) { - buf = heap_caps_aligned_alloc(4, size_bytes, MALLOC_CAP_DEFAULT); + buf = heap_caps_aligned_alloc(LV_DRAW_BUF_ALIGN, size_bytes, MALLOC_CAP_DEFAULT); } return buf; #else @@ -173,7 +177,13 @@ static void* lvgl_display_fb_base(struct LvglDisplayCtx* ctx, const uint8_t* col uint32_t area_size_px = (uint32_t)(x2 - x1 + 1) * (uint32_t)(y2 - y1 + 1); lv_display_rotation_t rotation = lv_display_get_rotation(disp); - if (ctx->sw_rotate && rotation != LV_DISPLAY_ROTATION_0) { + bool rotating = ctx->sw_rotate && rotation != LV_DISPLAY_ROTATION_0; + + // In FULL mode, a refresh cycle can call this once per still-unjoined invalidated area (see + // the comment below) before the frame is complete - rotating per-tile here would only ever + // reflect the last tile written, not the accumulated whole frame. Rotate the whole buffer in + // one shot instead, right before presenting (see the FULL-mode branch below). + if (rotating && ctx->render_mode != LV_DISPLAY_RENDER_MODE_FULL) { // sw_rotate is only ever requested for displays lacking real HW mirror/swap_xy capability // (see lvgl_devices.c), and lvgl_display_add() only binds fb-direct (owns_buffers == false) // when that capability is present - so this is always the owns_buffers == true case. @@ -227,6 +237,21 @@ static void* lvgl_display_fb_base(struct LvglDisplayCtx* ctx, const uint8_t* col } uint16_t hres = display_get_resolution_x(ctx->device); uint16_t vres = display_get_resolution_y(ctx->device); + + if (rotating) { + // fb_base now holds the whole completed frame, but still in LVGL's *logical* + // (rotated) w/h - rotate it as one block into rotate_buf, matching the panel's + // fixed physical w/h, before presenting. + lv_color_format_t color_format = lv_display_get_color_format(disp); + bool swapped_wh = rotation == LV_DISPLAY_ROTATION_90 || rotation == LV_DISPLAY_ROTATION_270; + int32_t logical_w = swapped_wh ? (int32_t)vres : (int32_t)hres; + int32_t logical_h = swapped_wh ? (int32_t)hres : (int32_t)vres; + uint32_t src_stride = lv_draw_buf_width_to_stride(logical_w, color_format); + uint32_t dest_stride = lv_draw_buf_width_to_stride(hres, color_format); + lv_draw_sw_rotate(fb_base, ctx->rotate_buf, logical_w, logical_h, src_stride, dest_stride, rotation, color_format); + fb_base = (uint8_t*)ctx->rotate_buf; + } + display_draw_bitmap(ctx->device, 0, 0, hres, vres, fb_base); } } else if (ctx->owns_buffers) { diff --git a/Tactility/Source/app/poweroff/PowerOff.cpp b/Tactility/Source/app/poweroff/PowerOff.cpp index 61a70fa4f..e9a559886 100644 --- a/Tactility/Source/app/poweroff/PowerOff.cpp +++ b/Tactility/Source/app/poweroff/PowerOff.cpp @@ -128,8 +128,8 @@ extern const AppManifest manifest = { .appId = "PowerOff", .appName = "Power Off", .appIcon = LVGL_ICON_SHARED_POWER_SETTINGS_NEW, - .appCategory = Category::Settings, - .appFlags = AppManifest::Flags::HideStatusBar, + .appCategory = Category::System, + .appFlags = AppManifest::Flags::HideStatusBar | AppManifest::Flags::Hidden, .createApp = create }; diff --git a/Tactility/Source/app/systeminfo/SystemInfo.cpp b/Tactility/Source/app/systeminfo/SystemInfo.cpp index 8e7f25d39..1c7b7f9d7 100644 --- a/Tactility/Source/app/systeminfo/SystemInfo.cpp +++ b/Tactility/Source/app/systeminfo/SystemInfo.cpp @@ -11,7 +11,6 @@ #include #include -#include #include #include #include @@ -58,22 +57,6 @@ static size_t getSpiTotal() { #endif } -static size_t getPsramMinFree() { -#ifdef ESP_PLATFORM - return heap_caps_get_minimum_free_size(MALLOC_CAP_SPIRAM); -#else - return 4096 * 1024; -#endif -} - -static size_t getPsramLargestBlock() { -#ifdef ESP_PLATFORM - return heap_caps_get_largest_free_block(MALLOC_CAP_SPIRAM); -#else - return 4096 * 1024; -#endif -} - enum class StorageUnit { Bytes, Kilobytes, @@ -212,20 +195,13 @@ static void clearContainer(lv_obj_t* container) { lv_obj_clean(container); } -static void addRtosTask(lv_obj_t* parent, const TaskStatus_t& task, uint32_t totalRuntime) { +static void addRtosTask(lv_obj_t* parent, const TaskStatus_t& task) { auto* label = lv_label_create(parent); const char* name = (task.pcTaskName == nullptr || task.pcTaskName[0] == 0) ? "(unnamed)" : task.pcTaskName; - - // If totalRuntime provided, show CPU percentage; otherwise just show state - if (totalRuntime > 0) { - float cpu_percent = (task.ulRunTimeCounter * 100.0f) / totalRuntime; - lv_label_set_text_fmt(label, "%s: %.1f%%", name, cpu_percent); - } else { - lv_label_set_text_fmt(label, "%s (%s)", name, getTaskState(task)); - } + lv_label_set_text_fmt(label, "%s (%s)", name, getTaskState(task)); } -static void updateRtosTasks(lv_obj_t* parent, bool showCpuPercent) { +static void updateRtosTasks(lv_obj_t* parent) { clearContainer(parent); UBaseType_t count = uxTaskGetNumberOfTasks(); @@ -238,15 +214,8 @@ static void updateRtosTasks(lv_obj_t* parent, bool showCpuPercent) { uint32_t totalRuntime = 0; UBaseType_t actual = uxTaskGetSystemState(tasks, count, &totalRuntime); - // Sort by CPU usage if showing percentages, otherwise keep original order - if (showCpuPercent) { - std::sort(tasks, tasks + actual, [](const TaskStatus_t& a, const TaskStatus_t& b) { - return a.ulRunTimeCounter > b.ulRunTimeCounter; - }); - } - for (int i = 0; i < actual; ++i) { - addRtosTask(parent, tasks[i], showCpuPercent ? totalRuntime : 0); + addRtosTask(parent, tasks[i]); } free(tasks); @@ -254,18 +223,6 @@ static void updateRtosTasks(lv_obj_t* parent, bool showCpuPercent) { #endif -static void addDevice(lv_obj_t* parent, const std::shared_ptr& device) { - auto* label = lv_label_create(parent); - lv_label_set_text(label, device->getName().c_str()); -} - -static void addDevices(lv_obj_t* parent) { - auto devices = hal::getDevices(); - for (const auto& device: devices) { - addDevice(parent, device); - } -} - static lv_obj_t* createTab(lv_obj_t* tabview, const char* name) { auto* tab = lv_tabview_add_tab(tabview, name); lv_obj_set_flex_flow(tab, LV_FLEX_FLOW_COLUMN); @@ -293,7 +250,6 @@ class SystemInfoApp final : public App { auto lock = lvgl::getSyncLock()->asScopedLock(); lock.lock(); app->updateMemory(); - app->updatePsram(); } }); @@ -313,11 +269,7 @@ class SystemInfoApp final : public App { MemoryBarWidgets systemStorageBar; lv_obj_t* tasksContainer = nullptr; - lv_obj_t* cpuContainer = nullptr; lv_obj_t* psramContainer = nullptr; - lv_obj_t* cpuSummaryLabel = nullptr; // Shows overall CPU utilization - lv_obj_t* taskCountLabel = nullptr; // Shows active task count - lv_obj_t* uptimeLabel = nullptr; // Shows system uptime bool hasExternalMem = false; bool hasDataStorage = false; @@ -358,212 +310,11 @@ class SystemInfoApp final : public App { void updateTasks() { #if configUSE_TRACE_FACILITY if (tasksContainer) { - updateRtosTasks(tasksContainer, false); // Tasks tab: show state - } - - if (cpuContainer) { - updateRtosTasks(cpuContainer, true); // CPU tab: show percentages - - // Update CPU summary at top of tab - // Note: FreeRTOS runtime stats accumulate since boot, so percentages - // are averages over entire uptime, not instantaneous usage - if (cpuSummaryLabel && taskCountLabel && uptimeLabel) { - UBaseType_t count = uxTaskGetNumberOfTasks(); - auto* tasks = (TaskStatus_t*)malloc(sizeof(TaskStatus_t) * count); - if (tasks) { - uint32_t totalRuntime = 0; - UBaseType_t actual = uxTaskGetSystemState(tasks, count, &totalRuntime); - - if (totalRuntime > 0 && actual > 0) { - // Calculate total CPU usage (100% - idle = usage) - uint32_t idleTime = 0; - for (int i = 0; i < actual; ++i) { - const char* name = tasks[i].pcTaskName; - if (name && (strcmp(name, "IDLE0") == 0 || strcmp(name, "IDLE1") == 0)) { - idleTime += tasks[i].ulRunTimeCounter; - } - } - - float cpuUsage = ((totalRuntime - idleTime) * 100.0f) / totalRuntime; - auto summary_text = std::format("Overall CPU Usage: {:.1f}% (avg since boot)", cpuUsage); - lv_label_set_text(cpuSummaryLabel, summary_text.c_str()); - - // Show total task count - auto core_text = std::format("Active Tasks: {} total", actual); - lv_label_set_text(taskCountLabel, core_text.c_str()); - - // Use actual system tick count for uptime - TickType_t ticks = xTaskGetTickCount(); - float uptime_sec = static_cast(ticks) / configTICK_RATE_HZ; - auto uptime_text = std::format("System Uptime: {:.1f} min", uptime_sec / 60.0f); - lv_label_set_text(uptimeLabel, uptime_text.c_str()); - } else { - lv_label_set_text(cpuSummaryLabel, "Overall CPU Usage: --.-%"); - lv_label_set_text(taskCountLabel, "Active Tasks: --"); - lv_label_set_text(uptimeLabel, "System Uptime: --"); - } - - free(tasks); - } - } + updateRtosTasks(tasksContainer); // Tasks tab: show state } #endif } - void updatePsram() { -#ifdef ESP_PLATFORM - if (!psramContainer || !hasExternalMem) return; - - clearContainer(psramContainer); - - size_t free_mem = getSpiFree(); - size_t total = getSpiTotal(); - size_t used = total - free_mem; - size_t min_free = getPsramMinFree(); - size_t largest_block = getPsramLargestBlock(); - size_t peak_usage = total - min_free; - - // Safety check - if no PSRAM, show error - if (total == 0) { - auto* error_label = lv_label_create(psramContainer); - lv_label_set_text(error_label, "No PSRAM detected"); - return; - } - - // Summary - auto* summary_label = lv_label_create(psramContainer); - lv_label_set_text(summary_label, "PSRAM Usage Summary"); - lv_obj_set_style_pad_bottom(summary_label, 8, 0); - - // Current usage - auto* usage_label = lv_label_create(psramContainer); - float used_mb = used / (1024.0f * 1024.0f); - float total_mb = total / (1024.0f * 1024.0f); - float used_percent = (used * 100.0f) / total; - auto usage_text = std::format("Current: {:.2f} / {:.2f} MB ({:.1f}% used)", - used_mb, total_mb, used_percent); - lv_label_set_text(usage_label, usage_text.c_str()); - - // Peak usage - auto* peak_label = lv_label_create(psramContainer); - float peak_mb = peak_usage / (1024.0f * 1024.0f); - float peak_percent = (peak_usage * 100.0f) / total; - auto peak_text = std::format("Peak: {:.2f} MB ({:.1f}% of total)", - peak_mb, peak_percent); - lv_label_set_text(peak_label, peak_text.c_str()); - - // Minimum free (lowest point) - auto* min_free_label = lv_label_create(psramContainer); - float min_free_mb = min_free / (1024.0f * 1024.0f); - auto min_free_text = std::format("Min Free: {:.2f} MB", min_free_mb); - lv_label_set_text(min_free_label, min_free_text.c_str()); - - // Largest contiguous block - auto* largest_label = lv_label_create(psramContainer); - float largest_mb = largest_block / (1024.0f * 1024.0f); - auto largest_text = std::format("Largest Block: {:.2f} MB", largest_mb); - lv_label_set_text(largest_label, largest_text.c_str()); - - // Spacer - auto* spacer = lv_obj_create(psramContainer); - lv_obj_set_size(spacer, LV_PCT(100), 16); - lv_obj_set_style_bg_opa(spacer, 0, 0); - lv_obj_set_style_border_width(spacer, 0, 0); - - // PSRAM Configuration section - auto* config_header = lv_label_create(psramContainer); - lv_label_set_text(config_header, "PSRAM Configuration"); - lv_obj_set_style_pad_bottom(config_header, 8, 0); - - // Get threshold from sdkconfig -#ifdef CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL - const int threshold = CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL; -#else - const int threshold = 16384; // Default ESP-IDF value -#endif - - // Display threshold configuration - auto* threshold_info = lv_label_create(psramContainer); - if (threshold >= 1024) { - lv_label_set_text_fmt(threshold_info, "• Threshold: >=%d KB -> PSRAM", threshold / 1024); - } else { - lv_label_set_text_fmt(threshold_info, "• Threshold: >=%d bytes -> PSRAM", threshold); - } - - auto* internal_info = lv_label_create(psramContainer); - if (threshold >= 1024) { - lv_label_set_text_fmt(internal_info, "• Allocations <%d KB -> Internal RAM", threshold / 1024); - } else { - lv_label_set_text_fmt(internal_info, "• Allocations <%d bytes -> Internal RAM", threshold); - } - - auto* note_label = lv_label_create(psramContainer); - lv_label_set_text(note_label, "• DMA buffers always use Internal RAM"); - - // Spacer after config - auto* spacer_config = lv_obj_create(psramContainer); - lv_obj_set_size(spacer_config, LV_PCT(100), 16); - lv_obj_set_style_bg_opa(spacer_config, 0, 0); - lv_obj_set_style_border_width(spacer_config, 0, 0); - - // Known PSRAM consumers header - auto* consumers_label = lv_label_create(psramContainer); - lv_label_set_text(consumers_label, "PSRAM Allocation Strategy"); - lv_obj_set_style_pad_bottom(consumers_label, 8, 0); - - // Explain what's in PSRAM - auto* strategy_note = lv_label_create(psramContainer); - lv_label_set_text(strategy_note, "Apps don't pre-allocate to PSRAM.\nThey use LVGL dynamic allocation:"); - lv_obj_set_style_text_color(strategy_note, lv_palette_main(LV_PALETTE_GREY), 0); - - // List what automatically goes to PSRAM - auto* lvgl_label = lv_label_create(psramContainer); - lv_label_set_text(lvgl_label, "• All LVGL widgets (buttons, labels, etc.)"); - - auto* framebuffer_label = lv_label_create(psramContainer); - lv_label_set_text(framebuffer_label, "• Display framebuffers"); - - auto* wifi_label = lv_label_create(psramContainer); - lv_label_set_text(wifi_label, "• WiFi/Network buffers"); - - auto* file_label = lv_label_create(psramContainer); - lv_label_set_text(file_label, "• File I/O buffers"); - - auto* task_label = lv_label_create(psramContainer); - lv_label_set_text(task_label, "• Task stacks (when enabled)"); - - auto* general_label = lv_label_create(psramContainer); - if (threshold >= 1024) { - lv_label_set_text_fmt(general_label, "• All allocations >=%d KB", threshold / 1024); - } else { - lv_label_set_text_fmt(general_label, "• All allocations >=%d bytes", threshold); - } - - // Spacer - auto* spacer_apps = lv_obj_create(psramContainer); - lv_obj_set_size(spacer_apps, LV_PCT(100), 16); - lv_obj_set_style_bg_opa(spacer_apps, 0, 0); - lv_obj_set_style_border_width(spacer_apps, 0, 0); - - // App behavior explanation - auto* app_behavior_label = lv_label_create(psramContainer); - lv_label_set_text(app_behavior_label, "App Memory Behavior"); - lv_obj_set_style_pad_bottom(app_behavior_label, 8, 0); - - auto* app_note1 = lv_label_create(psramContainer); - lv_label_set_text(app_note1, "• Apps allocate UI when opened (10-50 KB)"); - - auto* app_note2 = lv_label_create(psramContainer); - lv_label_set_text(app_note2, "• All app UI goes to PSRAM automatically"); - - auto* app_note3 = lv_label_create(psramContainer); - lv_label_set_text(app_note3, "• Apps deallocate when closed (no caching)"); - - auto* app_note4 = lv_label_create(psramContainer); - lv_label_set_text(app_note4, "• One app open at a time = 10-50 KB in PSRAM"); -#endif - } - void onShow(AppContext& app, lv_obj_t* parent) override { lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT); @@ -583,11 +334,8 @@ class SystemInfoApp final : public App { // Create tabs auto* memory_tab = createTab(tabview, "Memory"); - auto* psram_tab = createTab(tabview, "PSRAM"); - auto* cpu_tab = createTab(tabview, "CPU"); auto* storage_tab = createTab(tabview, "Storage"); auto* tasks_tab = createTab(tabview, "Tasks"); - auto* devices_tab = createTab(tabview, "Devices"); auto* about_tab = createTab(tabview, "About"); // Memory tab content @@ -598,16 +346,6 @@ class SystemInfoApp final : public App { externalMemBar = createMemoryBar(memory_tab, "External"); } - // PSRAM tab content (only if PSRAM exists) - if (hasExternalMem) { - psramContainer = lv_obj_create(psram_tab); - lv_obj_set_size(psramContainer, LV_PCT(100), LV_SIZE_CONTENT); - lv_obj_set_style_pad_all(psramContainer, 8, LV_STATE_DEFAULT); - lv_obj_set_style_border_width(psramContainer, 0, LV_STATE_DEFAULT); - lv_obj_set_flex_flow(psramContainer, LV_FLEX_FLOW_COLUMN); - lv_obj_set_style_bg_opa(psramContainer, 0, LV_STATE_DEFAULT); - } - #ifdef ESP_PLATFORM // Storage tab content uint64_t storage_total = 0; @@ -632,26 +370,6 @@ class SystemInfoApp final : public App { #endif #if configUSE_TRACE_FACILITY - // CPU tab - summary at top - cpuSummaryLabel = lv_label_create(cpu_tab); - lv_label_set_text(cpuSummaryLabel, "Overall CPU Usage: --.-%"); - lv_obj_set_style_pad_bottom(cpuSummaryLabel, 4, 0); - - taskCountLabel = lv_label_create(cpu_tab); - lv_label_set_text(taskCountLabel, "Active Tasks: --.-%"); - - uptimeLabel = lv_label_create(cpu_tab); - lv_label_set_text(uptimeLabel, "System Uptime: --.-%"); - lv_obj_set_style_pad_bottom(uptimeLabel, 8, 0); - - // CPU tab - container for task list (dynamic updates) - cpuContainer = lv_obj_create(cpu_tab); - lv_obj_set_size(cpuContainer, LV_PCT(100), LV_SIZE_CONTENT); - lv_obj_set_style_pad_all(cpuContainer, 8, LV_STATE_DEFAULT); - lv_obj_set_style_border_width(cpuContainer, 0, LV_STATE_DEFAULT); - lv_obj_set_flex_flow(cpuContainer, LV_FLEX_FLOW_COLUMN); - lv_obj_set_style_bg_opa(cpuContainer, 0, LV_STATE_DEFAULT); - // Tasks tab - container for dynamic updates tasksContainer = lv_obj_create(tasks_tab); lv_obj_set_size(tasksContainer, LV_PCT(100), LV_SIZE_CONTENT); @@ -661,8 +379,6 @@ class SystemInfoApp final : public App { lv_obj_set_style_bg_opa(tasksContainer, 0, LV_STATE_DEFAULT); #endif - addDevices(devices_tab); - // Build info auto* tactility_version = lv_label_create(about_tab); lv_label_set_text_fmt(tactility_version, "Tactility v%s", TT_VERSION); @@ -671,14 +387,18 @@ class SystemInfoApp final : public App { lv_label_set_text_fmt(esp_idf_version, "ESP-IDF v%d.%d.%d", ESP_IDF_VERSION_MAJOR, ESP_IDF_VERSION_MINOR, ESP_IDF_VERSION_PATCH); #endif + auto* device_vendor = lv_label_create(about_tab); + lv_label_set_text_fmt(device_vendor, "Hardware vendor: %s", CONFIG_TT_DEVICE_VENDOR); + auto* device_device_name = lv_label_create(about_tab); + lv_label_set_text_fmt(device_device_name, "Hardware model: %s", CONFIG_TT_DEVICE_NAME_SIMPLE); + // Initial updates updateMemory(); updateStorage(); // Storage: one-time update on show (doesn't change frequently) updateTasks(); - updatePsram(); // PSRAM: detailed breakdown // Start timers (only run while app is visible, stopped in onHide) - memoryTimer.start(); // Memory & PSRAM: every 10s + memoryTimer.start(); // Memory: every 10s tasksTimer.start(); // Tasks/CPU: every 15s } diff --git a/Tactility/Source/lvgl/Keyboard.cpp b/Tactility/Source/lvgl/Keyboard.cpp index 1894dc9ee..5ed5cb0c9 100644 --- a/Tactility/Source/lvgl/Keyboard.cpp +++ b/Tactility/Source/lvgl/Keyboard.cpp @@ -47,7 +47,18 @@ void software_keyboard_deactivate() { } bool hardware_keyboard_is_available() { - return keyboard_device != nullptr || device_find_first_by_type(&KEYBOARD_TYPE) != nullptr; + if (keyboard_device != nullptr) { + return true; + } + bool has_kernel_keyboard = false; + device_for_each_of_type(&KEYBOARD_TYPE, &has_kernel_keyboard, [](Device* device, void* context) { + if (device_is_ready(device)) { + *static_cast(context) = true; + return false; + } + return true; + }); + return has_kernel_keyboard; } void hardware_keyboard_set_indev(lv_indev_t* device) {