From 70ce15c7cf429cd2641f5e44534c2ab40075d85f Mon Sep 17 00:00:00 2001 From: Ken Van Hoeylandt Date: Sat, 18 Jul 2026 00:38:46 +0200 Subject: [PATCH 1/4] Device migrations, drivers and fixes --- .../DevicetreeCompiler/source/generator.py | 40 +- .../tests/test_integration.py | 90 ++- Devices/cyd-4848s040c/CMakeLists.txt | 5 +- .../cyd-4848s040c/Source/Configuration.cpp | 21 - .../Source/devices/St7701Display.cpp | 260 --------- .../Source/devices/St7701Display.h | 36 -- Devices/cyd-4848s040c/cyd,4848s040c.dts | 138 ++++- Devices/cyd-4848s040c/device.properties | 2 + Devices/cyd-4848s040c/devicetree.yaml | 2 + .../{Source => source}/module.cpp | 4 +- Devices/guition-jc1060p470ciwy/CMakeLists.txt | 6 +- .../Source/Configuration.cpp | 17 - .../Source/devices/Display.cpp | 66 --- .../Source/devices/Jd9165Display.cpp | 203 ------- .../Source/devices/Jd9165Display.h | 39 -- .../Source/devices/Power.cpp | 180 ------ .../Source/devices/Power.h | 7 - .../guition-jc1060p470ciwy/device.properties | 2 + .../guition-jc1060p470ciwy/devicetree.yaml | 2 + .../guition,jc1060p470ciwy.dts | 128 ++++- .../{Source => source}/module.cpp | 4 +- Devices/guition-jc2432w328c/CMakeLists.txt | 5 +- .../Source/Configuration.cpp | 21 - .../Source/devices/Display.cpp | 43 -- .../Source/devices/Display.h | 22 - Devices/guition-jc2432w328c/device.properties | 2 + Devices/guition-jc2432w328c/devicetree.yaml | 2 + .../guition,jc2432w328c.dts | 38 +- .../{Source => source}/module.cpp | 0 Devices/guition-jc3248w535c/CMakeLists.txt | 5 +- .../Source/Axs15231b/Axs15231bDisplay.cpp | 493 ---------------- .../Source/Axs15231b/Axs15231bDisplay.h | 139 ----- .../Source/Axs15231b/Axs15231bTouch.cpp | 38 -- .../Source/Axs15231b/Axs15231bTouch.h | 72 --- .../Source/Configuration.cpp | 21 - .../Source/devices/Display.cpp | 44 -- .../Source/devices/Display.h | 6 - Devices/guition-jc3248w535c/device.properties | 2 + Devices/guition-jc3248w535c/devicetree.yaml | 1 + .../guition,jc3248w535c.dts | 101 +++- .../{Source => source}/module.cpp | 2 - Devices/guition-jc8048w550c/CMakeLists.txt | 5 +- .../Source/Configuration.cpp | 21 - .../Source/devices/Display.cpp | 108 ---- .../Source/devices/Display.h | 5 - Devices/guition-jc8048w550c/device.properties | 2 + Devices/guition-jc8048w550c/devicetree.yaml | 2 + .../guition,jc8048w550c.dts | 68 +++ .../{Source => source}/module.cpp | 2 - Devices/heltec-wifi-lora-32-v3/CMakeLists.txt | 5 +- .../Source/Configuration.cpp | 48 -- .../Source/devices/Constants.h | 16 - .../Source/devices/Display.cpp | 19 - .../Source/devices/Display.h | 5 - .../Source/devices/Power.cpp | 20 - .../Source/devices/Power.h | 7 - .../heltec-wifi-lora-32-v3/device.properties | 2 + .../heltec-wifi-lora-32-v3/devicetree.yaml | 2 + .../heltec,wifi-lora-32-v3.dts | 81 +++ .../{Source => source}/module.cpp | 2 - Devices/lilygo-tdeck-max/CMakeLists.txt | 6 +- .../lilygo-tdeck-max/Source/Configuration.cpp | 100 ---- .../Source/devices/Cst66xxTouch.cpp | 225 -------- .../Source/devices/Cst66xxTouch.h | 54 -- .../Source/devices/Display.cpp | 57 -- .../lilygo-tdeck-max/Source/devices/Display.h | 5 - .../Source/devices/TdeckmaxKeyboard.cpp | 249 -------- .../Source/devices/TdeckmaxKeyboard.h | 69 --- .../Source/devices/TdeckmaxPower.cpp | 139 ----- .../Source/devices/TdeckmaxPower.h | 42 -- Devices/lilygo-tdeck-max/devicetree.yaml | 4 + Devices/lilygo-tdeck-max/lilygo,tdeck-max.dts | 88 +++ .../lilygo-tdeck-max/source/Configuration.cpp | 35 ++ .../source/devices/Display.cpp | 29 + .../source}/devices/Display.h | 0 .../{Source => source}/module.cpp | 0 Devices/lilygo-tdisplay-s3/CMakeLists.txt | 5 +- .../Source/Configuration.cpp | 22 - Devices/lilygo-tdisplay-s3/Source/Init.cpp | 44 -- .../Source/devices/Display.cpp | 30 - .../Source/devices/Display.h | 26 - .../Source/devices/Power.cpp | 12 - .../lilygo-tdisplay-s3/Source/devices/Power.h | 9 - Devices/lilygo-tdisplay-s3/Source/module.cpp | 23 - Devices/lilygo-tdisplay-s3/device.properties | 2 + Devices/lilygo-tdisplay-s3/devicetree.yaml | 2 + .../lilygo-tdisplay-s3/lilygo,tdisplay-s3.dts | 75 +++ Devices/lilygo-tdisplay-s3/source/module.cpp | 45 ++ Devices/lilygo-tdisplay/CMakeLists.txt | 5 +- .../lilygo-tdisplay/Source/Configuration.cpp | 23 - .../Source/devices/Display.cpp | 32 -- .../lilygo-tdisplay/Source/devices/Display.h | 18 - Devices/lilygo-tdisplay/device.properties | 2 + Devices/lilygo-tdisplay/devicetree.yaml | 2 + Devices/lilygo-tdisplay/lilygo,tdisplay.dts | 43 +- .../{Source => source}/module.cpp | 0 Devices/lilygo-tdongle-s3/CMakeLists.txt | 5 +- .../Source/Configuration.cpp | 18 - Devices/lilygo-tdongle-s3/Source/Init.cpp | 16 - .../Source/devices/Display.cpp | 36 -- .../Source/devices/Display.h | 5 - Devices/lilygo-tdongle-s3/device.properties | 2 + Devices/lilygo-tdongle-s3/devicetree.yaml | 1 + .../lilygo-tdongle-s3/lilygo,tdongle-s3.dts | 36 +- .../{Source => source}/module.cpp | 0 Devices/lilygo-tlora-pager/CMakeLists.txt | 6 +- .../Source/Configuration.cpp | 38 -- Devices/lilygo-tlora-pager/Source/Init.cpp | 57 -- .../Source/bindings/tlora_pager.h | 15 - .../Source/devices/Display.cpp | 33 -- .../Source/devices/Display.h | 5 - .../Source/devices/TpagerEncoder.cpp | 213 ------- .../Source/devices/TpagerEncoder.h | 30 - .../Source/devices/TpagerKeyboard.cpp | 219 ------- .../Source/devices/TpagerKeyboard.h | 52 -- .../Source/devices/TpagerPower.cpp | 77 --- .../Source/devices/TpagerPower.h | 25 - .../Source/drivers/TloraPager.cpp | 30 - .../Source/drivers/TloraPager.h | 11 - Devices/lilygo-tlora-pager/devicetree.yaml | 7 +- .../lilygo-tlora-pager/lilygo,tlora-pager.dts | 139 ++++- .../lilygo,tlora-pager.yaml | 5 - Devices/lilygo-tlora-pager/source/module.cpp | 72 +++ Documentation/ideas.md | 1 + Drivers/BQ25896/CMakeLists.txt | 5 - Drivers/BQ25896/README.md | 5 - Drivers/BQ25896/Source/Bq25896.cpp | 15 - Drivers/BQ25896/Source/Bq25896.h | 22 - Drivers/BQ27220/CMakeLists.txt | 5 - Drivers/BQ27220/README.md | 6 - Drivers/BQ27220/Source/Bq27220.cpp | 370 ------------ Drivers/BQ27220/Source/Bq27220.h | 106 ---- Drivers/DRV2605/CMakeLists.txt | 5 - Drivers/DRV2605/README.md | 5 - Drivers/DRV2605/Source/Drv2605.cpp | 96 ---- Drivers/DRV2605/Source/Drv2605.h | 83 --- Drivers/SSD1306/CMakeLists.txt | 5 - Drivers/SSD1306/README.md | 3 - Drivers/SSD1306/Source/Ssd1306Display.cpp | 216 ------- Drivers/SSD1306/Source/Ssd1306Display.h | 89 --- Drivers/ST7735/CMakeLists.txt | 5 - Drivers/ST7735/README.md | 3 - Drivers/ST7735/Source/St7735Display.cpp | 170 ------ Drivers/ST7735/Source/St7735Display.h | 109 ---- Drivers/ST7796/CMakeLists.txt | 5 - Drivers/ST7796/README.md | 4 - Drivers/ST7796/Source/St7796Display.cpp | 173 ------ Drivers/ST7796/Source/St7796Display.h | 105 ---- Drivers/TCA8418/Adafruit_TCA8418-license.txt | 26 - Drivers/TCA8418/CMakeLists.txt | 5 - Drivers/TCA8418/ORIGINAL-COPYRIGHT.md | 18 - Drivers/TCA8418/README.md | 5 - Drivers/TCA8418/Source/Tca8418.cpp | 227 -------- Drivers/TCA8418/Source/Tca8418.h | 72 --- Drivers/TCA9534/.gitignore | 56 -- Drivers/TCA9534/CMakeLists.txt | 7 - Drivers/TCA9534/Kconfig.projbuild | 17 - Drivers/TCA9534/LICENSE | 21 - Drivers/TCA9534/Makefile | 8 - Drivers/TCA9534/README.md | 49 -- Drivers/TCA9534/component.mk | 3 - Drivers/TCA9534/examples/CMakeLists.txt | 6 - Drivers/TCA9534/examples/read/read_tca9534.c | 59 -- Drivers/TCA9534/include/TCA9534.h | 170 ------ Drivers/TCA9534/src/TCA9534.c | 151 ----- Drivers/axs15231b-module/CMakeLists.txt | 11 + .../axs15231b-module/LICENSE-Apache-2.0.md | 195 +++++++ .../bindings/axs,axs15231b-touch.yaml | 45 ++ .../bindings/axs,axs15231b.yaml | 67 +++ Drivers/axs15231b-module/devicetree.yaml | 3 + .../include/axs15231b_module.h | 14 + .../include/bindings/axs15231b_display.h | 10 + .../include/bindings/axs15231b_touch.h | 10 + .../include/drivers/axs15231b_display.h | 46 ++ .../include/drivers/axs15231b_touch.h | 31 + .../source/axs15231b_display.cpp | 478 +++++++++++++++ .../source/axs15231b_touch.cpp | 211 +++++++ Drivers/axs15231b-module/source/module.cpp | 35 ++ Drivers/bq25896-module/CMakeLists.txt | 11 + .../bq25896-module/bindings/ti,bq25896.yaml | 11 + Drivers/bq25896-module/devicetree.yaml | 3 + .../bq25896-module/include/bindings/bq25896.h | 7 + .../bq25896-module/include/bq25896_module.h | 14 + .../bq25896-module/include/drivers/bq25896.h | 17 + Drivers/bq25896-module/source/bq25896.cpp | 175 ++++++ Drivers/bq25896-module/source/module.cpp | 35 ++ Drivers/bq27220-module/CMakeLists.txt | 11 + .../bq27220-module/bindings/ti,bq27220.yaml | 12 + Drivers/bq27220-module/devicetree.yaml | 3 + .../bq27220-module/include/bindings/bq27220.h | 7 + .../bq27220-module/include/bq27220_module.h | 14 + .../bq27220-module/include/drivers/bq27220.h | 17 + Drivers/bq27220-module/source/bq27220.cpp | 214 +++++++ Drivers/bq27220-module/source/module.cpp | 35 ++ Drivers/cst328-module/CMakeLists.txt | 11 + .../bindings/hynitron,cst328.yaml | 32 ++ Drivers/cst328-module/devicetree.yaml | 3 + .../cst328-module/include/bindings/cst328.h | 7 + Drivers/cst328-module/include/cst328_module.h | 14 + .../cst328-module/include/drivers/cst328.h | 25 + Drivers/cst328-module/source/cst328.cpp | 293 ++++++++++ .../cst328-module/source}/module.cpp | 13 +- Drivers/cst66xx-module/CMakeLists.txt | 11 + .../bindings/hynitron,cst66xx.yaml | 41 ++ Drivers/cst66xx-module/devicetree.yaml | 3 + .../cst66xx-module/include/bindings/cst66xx.h | 7 + .../cst66xx-module/include/cst66xx_module.h | 14 + .../cst66xx-module/include/drivers/cst66xx.h | 27 + Drivers/cst66xx-module/source/cst66xx.cpp | 266 +++++++++ Drivers/cst66xx-module/source/module.cpp | 32 ++ Drivers/drv2605-module/CMakeLists.txt | 11 + .../drv2605-module/bindings/ti,drv2605.yaml | 23 + Drivers/drv2605-module/devicetree.yaml | 3 + .../drv2605-module/include/bindings/drv2605.h | 7 + .../drv2605-module/include/drivers/drv2605.h | 23 + .../drv2605-module/include/drv2605_module.h | 14 + Drivers/drv2605-module/source/drv2605.cpp | 151 +++++ Drivers/drv2605-module/source/module.cpp | 28 + Drivers/jd9165-module/CMakeLists.txt | 11 + Drivers/jd9165-module/LICENSE-Apache-2.0.md | 195 +++++++ .../jd9165-module/bindings/jdi,jd9165.yaml | 126 ++++ Drivers/jd9165-module/devicetree.yaml | 3 + .../jd9165-module/include/bindings/jd9165.h | 10 + .../jd9165-module/include/drivers/jd9165.h | 69 +++ Drivers/jd9165-module/include/jd9165_module.h | 14 + Drivers/jd9165-module/source/jd9165.cpp | 525 +++++++++++++++++ Drivers/jd9165-module/source/module.cpp | 32 ++ Drivers/lilygo-module/CMakeLists.txt | 2 +- .../bindings/lilygo,tpager-encoder.yaml | 22 + .../include/lilygo/bindings/tpager_encoder.h | 7 + .../include/lilygo/drivers/tpager_encoder.h | 59 ++ .../lilygo/drivers/tpager_encoder_input.h | 20 + Drivers/lilygo-module/source/module.cpp | 3 + .../lilygo-module/source/tpager_encoder.cpp | 213 +++++++ .../source/tpager_encoder_input.cpp | 95 +++ Drivers/ssd1306-module/CMakeLists.txt | 11 + Drivers/ssd1306-module/LICENSE-Apache-2.0.md | 195 +++++++ .../bindings/solomon,ssd1306.yaml | 52 ++ Drivers/ssd1306-module/devicetree.yaml | 3 + .../ssd1306-module/include/bindings/ssd1306.h | 10 + .../ssd1306-module/include/drivers/ssd1306.h | 38 ++ .../ssd1306-module/include/ssd1306_module.h | 14 + Drivers/ssd1306-module/source/module.cpp | 32 ++ Drivers/ssd1306-module/source/ssd1306.cpp | 360 ++++++++++++ Drivers/st7701-module/CMakeLists.txt | 11 + Drivers/st7701-module/LICENSE-Apache-2.0.md | 195 +++++++ .../bindings/sitronix,st7701.yaml | 263 +++++++++ Drivers/st7701-module/devicetree.yaml | 3 + .../st7701-module/include/bindings/st7701.h | 10 + .../st7701-module/include/drivers/st7701.h | 111 ++++ Drivers/st7701-module/include/st7701_module.h | 14 + Drivers/st7701-module/source/module.cpp | 32 ++ Drivers/st7701-module/source/st7701.cpp | 543 ++++++++++++++++++ Drivers/st7735-module/CMakeLists.txt | 11 + .../bindings/sitronix,st7735.yaml | 77 +++ Drivers/st7735-module/devicetree.yaml | 3 + .../st7735-module/include/bindings/st7735.h | 7 + .../st7735-module/include/drivers/st7735.h | 38 ++ Drivers/st7735-module/include/st7735_module.h | 14 + Drivers/st7735-module/source/module.cpp | 32 ++ Drivers/st7735-module/source/st7735.cpp | 346 +++++++++++ Drivers/st7789-module/source/st7789.cpp | 22 +- Drivers/st7796-module/source/st7796.cpp | 19 +- Drivers/sy6970-module/CMakeLists.txt | 11 + .../bindings/silergy,sy6970.yaml | 21 + Drivers/sy6970-module/devicetree.yaml | 3 + .../sy6970-module/include/bindings/sy6970.h | 7 + .../sy6970-module/include/drivers/sy6970.h | 19 + Drivers/sy6970-module/include/sy6970_module.h | 14 + Drivers/sy6970-module/source/module.cpp | 35 ++ Drivers/sy6970-module/source/sy6970.cpp | 251 ++++++++ Drivers/tca8418-module/CMakeLists.txt | 11 + .../tca8418-module/bindings/ti,tca8418.yaml | 74 +++ Drivers/tca8418-module/devicetree.yaml | 3 + .../tca8418-module/include/bindings/tca8418.h | 7 + .../tca8418-module/include/drivers/tca8418.h | 35 ++ .../tca8418-module/include/tca8418_module.h | 14 + Drivers/tca8418-module/source/module.cpp | 32 ++ Drivers/tca8418-module/source/tca8418.cpp | 284 +++++++++ .../include/tactility/lvgl_display.h | 9 + Modules/lvgl-module/source/lvgl_devices.c | 9 +- Modules/lvgl-module/source/lvgl_display.c | 149 +++-- Modules/lvgl-module/source/lvgl_pointer.c | 1 - Tactility/Source/app/poweroff/PowerOff.cpp | 27 +- .../include/tactility/drivers/display.h | 30 + .../include/tactility/drivers/haptic.h | 70 +++ .../include/tactility/drivers/pointer.h | 4 + TactilityKernel/source/drivers/display.cpp | 18 +- TactilityKernel/source/drivers/haptic.cpp | 33 ++ TactilityKernel/source/drivers/pointer.cpp | 12 +- 290 files changed, 9055 insertions(+), 6716 deletions(-) delete mode 100644 Devices/cyd-4848s040c/Source/Configuration.cpp delete mode 100644 Devices/cyd-4848s040c/Source/devices/St7701Display.cpp delete mode 100644 Devices/cyd-4848s040c/Source/devices/St7701Display.h rename Devices/cyd-4848s040c/{Source => source}/module.cpp (78%) delete mode 100644 Devices/guition-jc1060p470ciwy/Source/Configuration.cpp delete mode 100644 Devices/guition-jc1060p470ciwy/Source/devices/Display.cpp delete mode 100644 Devices/guition-jc1060p470ciwy/Source/devices/Jd9165Display.cpp delete mode 100644 Devices/guition-jc1060p470ciwy/Source/devices/Jd9165Display.h delete mode 100644 Devices/guition-jc1060p470ciwy/Source/devices/Power.cpp delete mode 100644 Devices/guition-jc1060p470ciwy/Source/devices/Power.h rename Devices/guition-jc1060p470ciwy/{Source => source}/module.cpp (75%) delete mode 100644 Devices/guition-jc2432w328c/Source/Configuration.cpp delete mode 100644 Devices/guition-jc2432w328c/Source/devices/Display.cpp delete mode 100644 Devices/guition-jc2432w328c/Source/devices/Display.h rename Devices/guition-jc2432w328c/{Source => source}/module.cpp (100%) delete mode 100644 Devices/guition-jc3248w535c/Source/Axs15231b/Axs15231bDisplay.cpp delete mode 100644 Devices/guition-jc3248w535c/Source/Axs15231b/Axs15231bDisplay.h delete mode 100644 Devices/guition-jc3248w535c/Source/Axs15231b/Axs15231bTouch.cpp delete mode 100644 Devices/guition-jc3248w535c/Source/Axs15231b/Axs15231bTouch.h delete mode 100644 Devices/guition-jc3248w535c/Source/Configuration.cpp delete mode 100644 Devices/guition-jc3248w535c/Source/devices/Display.cpp delete mode 100644 Devices/guition-jc3248w535c/Source/devices/Display.h rename Devices/guition-jc3248w535c/{Source => source}/module.cpp (88%) delete mode 100644 Devices/guition-jc8048w550c/Source/Configuration.cpp delete mode 100644 Devices/guition-jc8048w550c/Source/devices/Display.cpp delete mode 100644 Devices/guition-jc8048w550c/Source/devices/Display.h rename Devices/guition-jc8048w550c/{Source => source}/module.cpp (88%) delete mode 100644 Devices/heltec-wifi-lora-32-v3/Source/Configuration.cpp delete mode 100644 Devices/heltec-wifi-lora-32-v3/Source/devices/Constants.h delete mode 100644 Devices/heltec-wifi-lora-32-v3/Source/devices/Display.cpp delete mode 100644 Devices/heltec-wifi-lora-32-v3/Source/devices/Display.h delete mode 100644 Devices/heltec-wifi-lora-32-v3/Source/devices/Power.cpp delete mode 100644 Devices/heltec-wifi-lora-32-v3/Source/devices/Power.h rename Devices/heltec-wifi-lora-32-v3/{Source => source}/module.cpp (88%) delete mode 100644 Devices/lilygo-tdeck-max/Source/Configuration.cpp delete mode 100644 Devices/lilygo-tdeck-max/Source/devices/Cst66xxTouch.cpp delete mode 100644 Devices/lilygo-tdeck-max/Source/devices/Cst66xxTouch.h delete mode 100644 Devices/lilygo-tdeck-max/Source/devices/Display.cpp delete mode 100644 Devices/lilygo-tdeck-max/Source/devices/Display.h delete mode 100644 Devices/lilygo-tdeck-max/Source/devices/TdeckmaxKeyboard.cpp delete mode 100644 Devices/lilygo-tdeck-max/Source/devices/TdeckmaxKeyboard.h delete mode 100644 Devices/lilygo-tdeck-max/Source/devices/TdeckmaxPower.cpp delete mode 100644 Devices/lilygo-tdeck-max/Source/devices/TdeckmaxPower.h create mode 100644 Devices/lilygo-tdeck-max/source/Configuration.cpp create mode 100644 Devices/lilygo-tdeck-max/source/devices/Display.cpp rename Devices/{guition-jc1060p470ciwy/Source => lilygo-tdeck-max/source}/devices/Display.h (100%) rename Devices/lilygo-tdeck-max/{Source => source}/module.cpp (100%) delete mode 100644 Devices/lilygo-tdisplay-s3/Source/Configuration.cpp delete mode 100644 Devices/lilygo-tdisplay-s3/Source/Init.cpp delete mode 100644 Devices/lilygo-tdisplay-s3/Source/devices/Display.cpp delete mode 100644 Devices/lilygo-tdisplay-s3/Source/devices/Display.h delete mode 100644 Devices/lilygo-tdisplay-s3/Source/devices/Power.cpp delete mode 100644 Devices/lilygo-tdisplay-s3/Source/devices/Power.h delete mode 100644 Devices/lilygo-tdisplay-s3/Source/module.cpp create mode 100644 Devices/lilygo-tdisplay-s3/source/module.cpp delete mode 100644 Devices/lilygo-tdisplay/Source/Configuration.cpp delete mode 100644 Devices/lilygo-tdisplay/Source/devices/Display.cpp delete mode 100644 Devices/lilygo-tdisplay/Source/devices/Display.h rename Devices/lilygo-tdisplay/{Source => source}/module.cpp (100%) delete mode 100644 Devices/lilygo-tdongle-s3/Source/Configuration.cpp delete mode 100644 Devices/lilygo-tdongle-s3/Source/Init.cpp delete mode 100644 Devices/lilygo-tdongle-s3/Source/devices/Display.cpp delete mode 100644 Devices/lilygo-tdongle-s3/Source/devices/Display.h rename Devices/lilygo-tdongle-s3/{Source => source}/module.cpp (100%) delete mode 100644 Devices/lilygo-tlora-pager/Source/Configuration.cpp delete mode 100644 Devices/lilygo-tlora-pager/Source/Init.cpp delete mode 100644 Devices/lilygo-tlora-pager/Source/bindings/tlora_pager.h delete mode 100644 Devices/lilygo-tlora-pager/Source/devices/Display.cpp delete mode 100644 Devices/lilygo-tlora-pager/Source/devices/Display.h delete mode 100644 Devices/lilygo-tlora-pager/Source/devices/TpagerEncoder.cpp delete mode 100644 Devices/lilygo-tlora-pager/Source/devices/TpagerEncoder.h delete mode 100644 Devices/lilygo-tlora-pager/Source/devices/TpagerKeyboard.cpp delete mode 100644 Devices/lilygo-tlora-pager/Source/devices/TpagerKeyboard.h delete mode 100644 Devices/lilygo-tlora-pager/Source/devices/TpagerPower.cpp delete mode 100644 Devices/lilygo-tlora-pager/Source/devices/TpagerPower.h delete mode 100644 Devices/lilygo-tlora-pager/Source/drivers/TloraPager.cpp delete mode 100644 Devices/lilygo-tlora-pager/Source/drivers/TloraPager.h delete mode 100644 Devices/lilygo-tlora-pager/lilygo,tlora-pager.yaml create mode 100644 Devices/lilygo-tlora-pager/source/module.cpp delete mode 100644 Drivers/BQ25896/CMakeLists.txt delete mode 100644 Drivers/BQ25896/README.md delete mode 100644 Drivers/BQ25896/Source/Bq25896.cpp delete mode 100644 Drivers/BQ25896/Source/Bq25896.h delete mode 100644 Drivers/BQ27220/CMakeLists.txt delete mode 100644 Drivers/BQ27220/README.md delete mode 100644 Drivers/BQ27220/Source/Bq27220.cpp delete mode 100644 Drivers/BQ27220/Source/Bq27220.h delete mode 100644 Drivers/DRV2605/CMakeLists.txt delete mode 100644 Drivers/DRV2605/README.md delete mode 100644 Drivers/DRV2605/Source/Drv2605.cpp delete mode 100644 Drivers/DRV2605/Source/Drv2605.h delete mode 100644 Drivers/SSD1306/CMakeLists.txt delete mode 100644 Drivers/SSD1306/README.md delete mode 100644 Drivers/SSD1306/Source/Ssd1306Display.cpp delete mode 100644 Drivers/SSD1306/Source/Ssd1306Display.h delete mode 100644 Drivers/ST7735/CMakeLists.txt delete mode 100644 Drivers/ST7735/README.md delete mode 100644 Drivers/ST7735/Source/St7735Display.cpp delete mode 100644 Drivers/ST7735/Source/St7735Display.h delete mode 100644 Drivers/ST7796/CMakeLists.txt delete mode 100644 Drivers/ST7796/README.md delete mode 100644 Drivers/ST7796/Source/St7796Display.cpp delete mode 100644 Drivers/ST7796/Source/St7796Display.h delete mode 100644 Drivers/TCA8418/Adafruit_TCA8418-license.txt delete mode 100644 Drivers/TCA8418/CMakeLists.txt delete mode 100644 Drivers/TCA8418/ORIGINAL-COPYRIGHT.md delete mode 100644 Drivers/TCA8418/README.md delete mode 100644 Drivers/TCA8418/Source/Tca8418.cpp delete mode 100644 Drivers/TCA8418/Source/Tca8418.h delete mode 100644 Drivers/TCA9534/.gitignore delete mode 100644 Drivers/TCA9534/CMakeLists.txt delete mode 100644 Drivers/TCA9534/Kconfig.projbuild delete mode 100644 Drivers/TCA9534/LICENSE delete mode 100644 Drivers/TCA9534/Makefile delete mode 100644 Drivers/TCA9534/README.md delete mode 100644 Drivers/TCA9534/component.mk delete mode 100644 Drivers/TCA9534/examples/CMakeLists.txt delete mode 100644 Drivers/TCA9534/examples/read/read_tca9534.c delete mode 100644 Drivers/TCA9534/include/TCA9534.h delete mode 100644 Drivers/TCA9534/src/TCA9534.c create mode 100644 Drivers/axs15231b-module/CMakeLists.txt create mode 100644 Drivers/axs15231b-module/LICENSE-Apache-2.0.md create mode 100644 Drivers/axs15231b-module/bindings/axs,axs15231b-touch.yaml create mode 100644 Drivers/axs15231b-module/bindings/axs,axs15231b.yaml create mode 100644 Drivers/axs15231b-module/devicetree.yaml create mode 100644 Drivers/axs15231b-module/include/axs15231b_module.h create mode 100644 Drivers/axs15231b-module/include/bindings/axs15231b_display.h create mode 100644 Drivers/axs15231b-module/include/bindings/axs15231b_touch.h create mode 100644 Drivers/axs15231b-module/include/drivers/axs15231b_display.h create mode 100644 Drivers/axs15231b-module/include/drivers/axs15231b_touch.h create mode 100644 Drivers/axs15231b-module/source/axs15231b_display.cpp create mode 100644 Drivers/axs15231b-module/source/axs15231b_touch.cpp create mode 100644 Drivers/axs15231b-module/source/module.cpp create mode 100644 Drivers/bq25896-module/CMakeLists.txt create mode 100644 Drivers/bq25896-module/bindings/ti,bq25896.yaml create mode 100644 Drivers/bq25896-module/devicetree.yaml create mode 100644 Drivers/bq25896-module/include/bindings/bq25896.h create mode 100644 Drivers/bq25896-module/include/bq25896_module.h create mode 100644 Drivers/bq25896-module/include/drivers/bq25896.h create mode 100644 Drivers/bq25896-module/source/bq25896.cpp create mode 100644 Drivers/bq25896-module/source/module.cpp create mode 100644 Drivers/bq27220-module/CMakeLists.txt create mode 100644 Drivers/bq27220-module/bindings/ti,bq27220.yaml create mode 100644 Drivers/bq27220-module/devicetree.yaml create mode 100644 Drivers/bq27220-module/include/bindings/bq27220.h create mode 100644 Drivers/bq27220-module/include/bq27220_module.h create mode 100644 Drivers/bq27220-module/include/drivers/bq27220.h create mode 100644 Drivers/bq27220-module/source/bq27220.cpp create mode 100644 Drivers/bq27220-module/source/module.cpp create mode 100644 Drivers/cst328-module/CMakeLists.txt create mode 100644 Drivers/cst328-module/bindings/hynitron,cst328.yaml create mode 100644 Drivers/cst328-module/devicetree.yaml create mode 100644 Drivers/cst328-module/include/bindings/cst328.h create mode 100644 Drivers/cst328-module/include/cst328_module.h create mode 100644 Drivers/cst328-module/include/drivers/cst328.h create mode 100644 Drivers/cst328-module/source/cst328.cpp rename {Devices/lilygo-tlora-pager/Source => Drivers/cst328-module/source}/module.cpp (71%) create mode 100644 Drivers/cst66xx-module/CMakeLists.txt create mode 100644 Drivers/cst66xx-module/bindings/hynitron,cst66xx.yaml create mode 100644 Drivers/cst66xx-module/devicetree.yaml create mode 100644 Drivers/cst66xx-module/include/bindings/cst66xx.h create mode 100644 Drivers/cst66xx-module/include/cst66xx_module.h create mode 100644 Drivers/cst66xx-module/include/drivers/cst66xx.h create mode 100644 Drivers/cst66xx-module/source/cst66xx.cpp create mode 100644 Drivers/cst66xx-module/source/module.cpp create mode 100644 Drivers/drv2605-module/CMakeLists.txt create mode 100644 Drivers/drv2605-module/bindings/ti,drv2605.yaml create mode 100644 Drivers/drv2605-module/devicetree.yaml create mode 100644 Drivers/drv2605-module/include/bindings/drv2605.h create mode 100644 Drivers/drv2605-module/include/drivers/drv2605.h create mode 100644 Drivers/drv2605-module/include/drv2605_module.h create mode 100644 Drivers/drv2605-module/source/drv2605.cpp create mode 100644 Drivers/drv2605-module/source/module.cpp create mode 100644 Drivers/jd9165-module/CMakeLists.txt create mode 100644 Drivers/jd9165-module/LICENSE-Apache-2.0.md create mode 100644 Drivers/jd9165-module/bindings/jdi,jd9165.yaml create mode 100644 Drivers/jd9165-module/devicetree.yaml create mode 100644 Drivers/jd9165-module/include/bindings/jd9165.h create mode 100644 Drivers/jd9165-module/include/drivers/jd9165.h create mode 100644 Drivers/jd9165-module/include/jd9165_module.h create mode 100644 Drivers/jd9165-module/source/jd9165.cpp create mode 100644 Drivers/jd9165-module/source/module.cpp create mode 100644 Drivers/lilygo-module/bindings/lilygo,tpager-encoder.yaml create mode 100644 Drivers/lilygo-module/include/lilygo/bindings/tpager_encoder.h create mode 100644 Drivers/lilygo-module/include/lilygo/drivers/tpager_encoder.h create mode 100644 Drivers/lilygo-module/include/lilygo/drivers/tpager_encoder_input.h create mode 100644 Drivers/lilygo-module/source/tpager_encoder.cpp create mode 100644 Drivers/lilygo-module/source/tpager_encoder_input.cpp create mode 100644 Drivers/ssd1306-module/CMakeLists.txt create mode 100644 Drivers/ssd1306-module/LICENSE-Apache-2.0.md create mode 100644 Drivers/ssd1306-module/bindings/solomon,ssd1306.yaml create mode 100644 Drivers/ssd1306-module/devicetree.yaml create mode 100644 Drivers/ssd1306-module/include/bindings/ssd1306.h create mode 100644 Drivers/ssd1306-module/include/drivers/ssd1306.h create mode 100644 Drivers/ssd1306-module/include/ssd1306_module.h create mode 100644 Drivers/ssd1306-module/source/module.cpp create mode 100644 Drivers/ssd1306-module/source/ssd1306.cpp create mode 100644 Drivers/st7701-module/CMakeLists.txt create mode 100644 Drivers/st7701-module/LICENSE-Apache-2.0.md create mode 100644 Drivers/st7701-module/bindings/sitronix,st7701.yaml create mode 100644 Drivers/st7701-module/devicetree.yaml create mode 100644 Drivers/st7701-module/include/bindings/st7701.h create mode 100644 Drivers/st7701-module/include/drivers/st7701.h create mode 100644 Drivers/st7701-module/include/st7701_module.h create mode 100644 Drivers/st7701-module/source/module.cpp create mode 100644 Drivers/st7701-module/source/st7701.cpp create mode 100644 Drivers/st7735-module/CMakeLists.txt create mode 100644 Drivers/st7735-module/bindings/sitronix,st7735.yaml create mode 100644 Drivers/st7735-module/devicetree.yaml create mode 100644 Drivers/st7735-module/include/bindings/st7735.h create mode 100644 Drivers/st7735-module/include/drivers/st7735.h create mode 100644 Drivers/st7735-module/include/st7735_module.h create mode 100644 Drivers/st7735-module/source/module.cpp create mode 100644 Drivers/st7735-module/source/st7735.cpp create mode 100644 Drivers/sy6970-module/CMakeLists.txt create mode 100644 Drivers/sy6970-module/bindings/silergy,sy6970.yaml create mode 100644 Drivers/sy6970-module/devicetree.yaml create mode 100644 Drivers/sy6970-module/include/bindings/sy6970.h create mode 100644 Drivers/sy6970-module/include/drivers/sy6970.h create mode 100644 Drivers/sy6970-module/include/sy6970_module.h create mode 100644 Drivers/sy6970-module/source/module.cpp create mode 100644 Drivers/sy6970-module/source/sy6970.cpp create mode 100644 Drivers/tca8418-module/CMakeLists.txt create mode 100644 Drivers/tca8418-module/bindings/ti,tca8418.yaml create mode 100644 Drivers/tca8418-module/devicetree.yaml create mode 100644 Drivers/tca8418-module/include/bindings/tca8418.h create mode 100644 Drivers/tca8418-module/include/drivers/tca8418.h create mode 100644 Drivers/tca8418-module/include/tca8418_module.h create mode 100644 Drivers/tca8418-module/source/module.cpp create mode 100644 Drivers/tca8418-module/source/tca8418.cpp create mode 100644 TactilityKernel/include/tactility/drivers/haptic.h create mode 100644 TactilityKernel/source/drivers/haptic.cpp diff --git a/Buildscripts/DevicetreeCompiler/source/generator.py b/Buildscripts/DevicetreeCompiler/source/generator.py index 3bd641a46..4a45cce72 100644 --- a/Buildscripts/DevicetreeCompiler/source/generator.py +++ b/Buildscripts/DevicetreeCompiler/source/generator.py @@ -161,7 +161,7 @@ def resolve_parameters_from_bindings(device: Device, bindings: list[Binding], de node_name = get_device_node_name_safe(device) result = [] - phandle_arrays = [] + array_decls = [] for binding_property in binding_properties: device_property = find_device_property(device, binding_property.name) @@ -172,7 +172,35 @@ def resolve_parameters_from_bindings(device: Device, bindings: list[Binding], de array_var = f"{node_name}_{prop_safe}" if device_property is not None: entries = resolve_phandle_array_entries(device_property, devices) - phandle_arrays.append((array_var, binding_property.element_type, entries)) + array_decls.append((array_var, binding_property.element_type, entries)) + result.append(f"({binding_property.element_type}*){array_var}") + result.append(str(len(entries))) + elif binding_property.default is not None: + result.append("NULL") + result.append("0") + elif binding_property.required: + raise DevicetreeException(f"device {device.node_name} doesn't have property '{binding_property.name}'") + else: + result.append("NULL") + result.append("0") + continue + + if binding_property.type == "array": + # A flat literal array (DTS `[ ... ]` syntax, e.g. a byte blob), as opposed to + # phandle-array's list of resolved device references. Emits the same + # (pointer, length) parameter pair, backed by a plain data array instead of one + # holding phandle-derived initializers. + if binding_property.element_type is None: + raise DevicetreeException(f"array property '{binding_property.name}' requires 'element-type' in binding") + prop_safe = binding_property.name.replace("-", "_") + array_var = f"{node_name}_{prop_safe}" + if device_property is not None: + if device_property.type != "array": + raise DevicetreeException( + f"Device '{device.node_name}' property '{binding_property.name}' must use '[ ... ]' array syntax" + ) + entries = [str(value) for value in device_property.value] + array_decls.append((array_var, binding_property.element_type, entries)) result.append(f"({binding_property.element_type}*){array_var}") result.append(str(len(entries))) elif binding_property.default is not None: @@ -207,17 +235,17 @@ def resolve_parameters_from_bindings(device: Device, bindings: list[Binding], de validate_property_range(device, binding_property, device_property.value) result.append(property_to_string(device_property, devices)) - return result, phandle_arrays + return result, array_decls def write_config(file, device: Device, bindings: list[Binding], devices: list[Device], type_name: str): node_name = get_device_node_name_safe(device) config_type = f"{type_name}_config_dt" config_variable_name = f"{node_name}_config" - config_params, phandle_arrays = resolve_parameters_from_bindings(device, bindings, devices) + config_params, array_decls = resolve_parameters_from_bindings(device, bindings, devices) - # Write phandle-array variables before the config struct - for array_var, element_type, entries in phandle_arrays: + # Write phandle-array/array variables before the config struct + for array_var, element_type, entries in array_decls: entries_str = ", ".join(entries) file.write(f"static {element_type} {array_var}[] = {{ {entries_str} }};\n") diff --git a/Buildscripts/DevicetreeCompiler/tests/test_integration.py b/Buildscripts/DevicetreeCompiler/tests/test_integration.py index d1c6f96f1..a5186e700 100644 --- a/Buildscripts/DevicetreeCompiler/tests/test_integration.py +++ b/Buildscripts/DevicetreeCompiler/tests/test_integration.py @@ -209,6 +209,92 @@ def test_minmax_symbolic_value_skips_validation(): print("PASSED") return True +def write_array_config(tmp_dir, device_property_line): + config_dir = os.path.join(tmp_dir, "array_data") + bindings_dir = os.path.join(config_dir, "bindings") + os.makedirs(bindings_dir) + + with open(os.path.join(config_dir, "devicetree.yaml"), "w") as f: + f.write("dts: test.dts\nbindings: bindings") + + with open(os.path.join(config_dir, "test.dts"), "w") as f: + f.write(f"""/dts-v1/; + +/ {{ + compatible = "test,root"; + model = "Test Model"; + + test-device@0 {{ + compatible = "test,array-device"; + {device_property_line} + }}; +}}; +""") + + with open(os.path.join(bindings_dir, "test,root.yaml"), "w") as f: + f.write("description: Test root binding\ncompatible: \"test,root\"\nproperties:\n model:\n type: string\n") + + with open(os.path.join(bindings_dir, "test,array-device.yaml"), "w") as f: + f.write("""description: Test array binding +compatible: "test,array-device" +properties: + init-sequence: + type: array + element-type: uint8_t +""") + + return config_dir + +def test_array_property_generates_static_array_and_length(): + print("Running test_array_property_generates_static_array_and_length...") + with tempfile.TemporaryDirectory() as tmp_dir: + config_dir = write_array_config(tmp_dir, "init-sequence = [0xFF 0x01 0x00 0x00 0x10 5 0];") + output_dir = os.path.join(tmp_dir, "output") + os.makedirs(output_dir) + + result = run_compiler(config_dir, output_dir) + + if result.returncode != 0: + print(f"FAILED: Compilation should have succeeded: {result.stderr} {result.stdout}") + return False + + with open(os.path.join(output_dir, "devicetree.c")) as f: + generated = f.read() + + if "static uint8_t test_device_init_sequence[] = { 0xFF, 0x01, 0x00, 0x00, 0x10, 5, 0 };" not in generated: + print(f"FAILED: Expected static array declaration not found:\n{generated}") + return False + + if "(uint8_t*)test_device_init_sequence" not in generated or "\t7\n" not in generated: + print(f"FAILED: Expected (pointer, length) config params not found:\n{generated}") + return False + + print("PASSED") + return True + +def test_array_property_defaults_to_null_when_absent(): + print("Running test_array_property_defaults_to_null_when_absent...") + with tempfile.TemporaryDirectory() as tmp_dir: + config_dir = write_array_config(tmp_dir, "") + output_dir = os.path.join(tmp_dir, "output") + os.makedirs(output_dir) + + result = run_compiler(config_dir, output_dir) + + if result.returncode != 0: + print(f"FAILED: Compilation should have succeeded: {result.stderr} {result.stdout}") + return False + + with open(os.path.join(output_dir, "devicetree.c")) as f: + generated = f.read() + + if "NULL,\n\t0" not in generated: + print(f"FAILED: Expected NULL/0 defaults not found:\n{generated}") + return False + + print("PASSED") + return True + def test_compile_missing_config(): print("Running test_compile_missing_config...") with tempfile.TemporaryDirectory() as output_dir: @@ -234,7 +320,9 @@ def test_compile_missing_config(): test_minmax_below_minimum_fails, test_minmax_above_maximum_fails, test_minmax_out_of_range_default_fails, - test_minmax_symbolic_value_skips_validation + test_minmax_symbolic_value_skips_validation, + test_array_property_generates_static_array_and_length, + test_array_property_defaults_to_null_when_absent ] failed = 0 diff --git a/Devices/cyd-4848s040c/CMakeLists.txt b/Devices/cyd-4848s040c/CMakeLists.txt index ae2c58a51..a832a83a2 100644 --- a/Devices/cyd-4848s040c/CMakeLists.txt +++ b/Devices/cyd-4848s040c/CMakeLists.txt @@ -1,7 +1,6 @@ -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 EspLcdCompat esp_lcd_st7701 esp_lcd_panel_io_additions GT911 PwmBacklight driver vfs fatfs + REQUIRES TactilityKernel driver ) diff --git a/Devices/cyd-4848s040c/Source/Configuration.cpp b/Devices/cyd-4848s040c/Source/Configuration.cpp deleted file mode 100644 index 28f7637c3..000000000 --- a/Devices/cyd-4848s040c/Source/Configuration.cpp +++ /dev/null @@ -1,21 +0,0 @@ -#include "devices/St7701Display.h" - -#include -#include - -using namespace tt::hal; - -static bool initBoot() { - return driver::pwmbacklight::init(GPIO_NUM_38, 1000); -} - -static DeviceVector createDevices() { - return { - std::make_shared(), - }; -} - -extern const Configuration hardwareConfiguration = { - .initBoot = initBoot, - .createDevices = createDevices -}; diff --git a/Devices/cyd-4848s040c/Source/devices/St7701Display.cpp b/Devices/cyd-4848s040c/Source/devices/St7701Display.cpp deleted file mode 100644 index 241ae3055..000000000 --- a/Devices/cyd-4848s040c/Source/devices/St7701Display.cpp +++ /dev/null @@ -1,260 +0,0 @@ -#include "St7701Display.h" - -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -constexpr auto* TAG = "St7701Display"; - -// GPIO47/48 are physically shared between this bit-banged 3-wire command bus -// and the SD card's real SPI2 bus (no alternate pins exist on this PCB). -// esp_lcd_new_panel_io_3wire_spi() reconfigures them as plain GPIO via -// gpio_config(), severing their SPI2 matrix routing. Reconnect them here once -// the vendor init sequence is done, so SD card reads/writes keep working. -// Safe only because nothing else calls into the ST7701 IO handle after boot. -static void reclaimSpiPinsForSdCard() { - esp_rom_gpio_connect_out_signal(GPIO_NUM_47, spi_periph_signal[SPI2_HOST].spid_out, false, false); - esp_rom_gpio_connect_out_signal(GPIO_NUM_48, spi_periph_signal[SPI2_HOST].spiclk_out, false, false); - gpio_set_direction(GPIO_NUM_47, GPIO_MODE_OUTPUT); - gpio_set_direction(GPIO_NUM_48, GPIO_MODE_OUTPUT); -} - -static const st7701_lcd_init_cmd_t st7701_lcd_init_cmds[] = { - // {cmd, { data }, data_size, delay_ms} - {0xFF, (uint8_t[]) {0x77, 0x01, 0x00, 0x00, 0x10}, 5, 0}, - {0xC0, (uint8_t[]) {0x3B, 0x00}, 2, 0}, - {0xC1, (uint8_t[]) {0x0D, 0x02}, 2, 0}, - {0xC2, (uint8_t[]) {0x31, 0x05}, 2, 0}, - {0xCD, (uint8_t[]) {0x00}, 1, 0}, //0x08 - //Positive Voltage Gamma Control - {0xB0, (uint8_t[]) {0x00, 0x11, 0x18, 0x0E, 0x11, 0x06, 0x07, 0x08, 0x07, 0x22, 0x04, 0x12, 0x0F, 0xAA, 0x31, 0x18}, 16, 0}, - //Negative Voltage Gamma Control - {0xB1, (uint8_t[]) {0x00, 0x11, 0x19, 0x0E, 0x12, 0x07, 0x08, 0x08, 0x08, 0x22, 0x04, 0x11, 0x11, 0xA9, 0x32, 0x18}, 16, 0}, - //Page1 - {0xFF, (uint8_t[]) {0x77, 0x01, 0x00, 0x00, 0x11}, 5, 0}, - {0xB0, (uint8_t[]) {0x60}, 1, 0}, //Vop=4.7375v - {0xB1, (uint8_t[]) {0x32}, 1, 0}, //VCOM=32 - {0xB2, (uint8_t[]) {0x07}, 1, 0}, //VGH=15v - {0xB3, (uint8_t[]) {0x80}, 1, 0}, - {0xB5, (uint8_t[]) {0x49}, 1, 0}, //VGL=-10.17v - {0xB7, (uint8_t[]) {0x85}, 1, 0}, - {0xB8, (uint8_t[]) {0x21}, 1, 0}, //AVDD=6.6 & AVCL=-4.6 - {0xC1, (uint8_t[]) {0x78}, 1, 0}, - {0xC2, (uint8_t[]) {0x78}, 1, 0}, - {0xE0, (uint8_t[]) {0x00, 0x1B, 0x02}, 3, 0}, - {0xE1, (uint8_t[]) {0x08, 0xA0, 0x00, 0x00, 0x07, 0xA0, 0x00, 0x00, 0x00, 0x44, 0x44}, 11, 0}, - {0xE2, (uint8_t[]) {0x11, 0x11, 0x44, 0x44, 0xED, 0xA0, 0x00, 0x00, 0xEC, 0xA0, 0x00, 0x00}, 12, 0}, - {0xE3, (uint8_t[]) {0x00, 0x00, 0x11, 0x11}, 4, 0}, - {0xE4, (uint8_t[]) {0x44, 0x44}, 2, 0}, - {0xE5, (uint8_t[]) {0x0A, 0xE9, 0xD8, 0xA0, 0x0C, 0xEB, 0xD8, 0xA0, 0x0E, 0xED, 0xD8, 0xA0, 0x10, 0xEF, 0xD8, 0xA0}, 16, 0}, - {0xE6, (uint8_t[]) {0x00, 0x00, 0x11, 0x11}, 4, 0}, - {0xE7, (uint8_t[]) {0x44, 0x44}, 2, 0}, - {0xE8, (uint8_t[]) {0x09, 0xE8, 0xD8, 0xA0, 0x0B, 0xEA, 0xD8, 0xA0, 0x0D, 0xEC, 0xD8, 0xA0, 0x0F, 0xEE, 0xD8, 0xA0}, 16, 0}, - {0xEB, (uint8_t[]) {0x02, 0x00, 0xE4, 0xE4, 0x88, 0x00, 0x40}, 7, 0}, - {0xEC, (uint8_t[]) {0x3C, 0x00}, 2, 0}, - {0xED, (uint8_t[]) {0xAB, 0x89, 0x76, 0x54, 0x02, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x20, 0x45, 0x67, 0x98, 0xBA}, 16, 0}, - //VAP & VAN - {0xFF, (uint8_t[]) {0x77, 0x01, 0x00, 0x00, 0x13}, 5, 0}, - {0xE5, (uint8_t[]) {0xE4}, 1, 0}, - {0xFF, (uint8_t[]) {0x77, 0x01, 0x00, 0x00, 0x00}, 5, 0}, - {0x3A, (uint8_t[]) {0x60}, 1, 10}, //0x70 RGB888, 0x60 RGB666, 0x50 RGB565 - {0x11, (uint8_t[]) {0x00}, 0, 120}, //Sleep Out - {0x29, (uint8_t[]) {0x00}, 0, 0}, //Display On -}; - -bool St7701Display::createIoHandle(esp_lcd_panel_io_handle_t& outHandle) { - spi_line_config_t line_config = { - .cs_io_type = IO_TYPE_GPIO, - .cs_gpio_num = GPIO_NUM_39, - .scl_io_type = IO_TYPE_GPIO, - .scl_gpio_num = GPIO_NUM_48, - .sda_io_type = IO_TYPE_GPIO, - .sda_gpio_num = GPIO_NUM_47, - .io_expander = nullptr, - }; - esp_lcd_panel_io_3wire_spi_config_t panel_io_config = ST7701_PANEL_IO_3WIRE_SPI_CONFIG(line_config, 0); - return esp_lcd_new_panel_io_3wire_spi(&panel_io_config, &outHandle) == ESP_OK; -} - -bool St7701Display::createPanelHandle(esp_lcd_panel_io_handle_t ioHandle, esp_lcd_panel_handle_t& panelHandle) { - const esp_lcd_rgb_panel_config_t rgb_config = { - .clk_src = LCD_CLK_SRC_DEFAULT, - .timings = { - .pclk_hz = 14000000, - .h_res = 480, - .v_res = 480, - .hsync_pulse_width = 10, - .hsync_back_porch = 10, - .hsync_front_porch = 20, - .vsync_pulse_width = 10, - .vsync_back_porch = 10, - .vsync_front_porch = 10, - .flags = { - .hsync_idle_low = false, - .vsync_idle_low = false, - .de_idle_high = false, - .pclk_active_neg = false, - .pclk_idle_high = false - } - }, - .data_width = 16, - .bits_per_pixel = 16, - .num_fbs = 2, - .bounce_buffer_size_px = 480 * 10, - .sram_trans_align = 8, - .psram_trans_align = 64, - .hsync_gpio_num = GPIO_NUM_16, - .vsync_gpio_num = GPIO_NUM_17, - .de_gpio_num = GPIO_NUM_18, - .pclk_gpio_num = GPIO_NUM_21, - .disp_gpio_num = GPIO_NUM_NC, - .data_gpio_nums = { - GPIO_NUM_4, // B1 - GPIO_NUM_5, // B2 - GPIO_NUM_6, // B3 - GPIO_NUM_7, // B4 - GPIO_NUM_15, // B5 - GPIO_NUM_8, // G1 - GPIO_NUM_20, // G2 - GPIO_NUM_3, // G3 - GPIO_NUM_46, // G4 - GPIO_NUM_9, // G5 - GPIO_NUM_10, // G6 - GPIO_NUM_11, // R1 - GPIO_NUM_12, // R2 - GPIO_NUM_13, // R3 - GPIO_NUM_14, // R4 - GPIO_NUM_0 // R5 - }, - .flags = { - .disp_active_low = false, - .refresh_on_demand = false, - .fb_in_psram = true, - .double_fb = true, - .no_fb = false, - .bb_invalidate_cache = true - } - }; - - st7701_vendor_config_t vendor_config = { - .init_cmds = st7701_lcd_init_cmds, - .init_cmds_size = sizeof(st7701_lcd_init_cmds) / sizeof(st7701_lcd_init_cmd_t), - .rgb_config = &rgb_config, - .flags = { - .use_mipi_interface = 0, - .mirror_by_cmd = 1, - .auto_del_panel_io = 0, - }, - }; - - const esp_lcd_panel_dev_config_t panel_config = { - .reset_gpio_num = GPIO_NUM_NC, - .rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB, - .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_st7701(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_invert_color(panelHandle, false) != ESP_OK) { - LOG_E(TAG, "Failed to invert color"); - return false; - } - - esp_lcd_panel_set_gap(panelHandle, 0, 0); - - if (esp_lcd_panel_disp_on_off(panelHandle, true) != ESP_OK) { - LOG_E(TAG, "Failed to turn display on"); - return false; - } - - reclaimSpiPinsForSdCard(); - - return true; -} - -lvgl_port_display_cfg_t St7701Display::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 = (480 * 480), - .double_buffer = true, - .trans_size = 0, - .hres = 480, - .vres = 480, - .monochrome = false, - .rotation = { - .swap_xy = false, - .mirror_x = false, - .mirror_y = false, - }, - .color_format = LV_COLOR_FORMAT_RGB565, - .flags = { - .buff_dma = false, - .buff_spiram = true, - .sw_rotate = false, - .swap_bytes = false, - .full_refresh = false, - .direct_mode = false - } - }; -} - -lvgl_port_display_rgb_cfg_t St7701Display::getLvglPortDisplayRgbConfig(esp_lcd_panel_io_handle_t ioHandle, esp_lcd_panel_handle_t panelHandle) { - return { - .flags = { - .bb_mode = true, - .avoid_tearing = false - } - }; -} - -std::shared_ptr St7701Display::getTouchDevice() { - if (touchDevice == nullptr) { - auto* i2c = device_find_by_name("i2c0"); - check(i2c); - auto configuration = std::make_unique( - i2c, - 480, - 480 - ); - - touchDevice = std::make_shared(std::move(configuration)); - } - - return touchDevice; -} - -void St7701Display::setBacklightDuty(uint8_t backlightDuty) { - driver::pwmbacklight::setBacklightDuty(backlightDuty); -} diff --git a/Devices/cyd-4848s040c/Source/devices/St7701Display.h b/Devices/cyd-4848s040c/Source/devices/St7701Display.h deleted file mode 100644 index 593885f8d..000000000 --- a/Devices/cyd-4848s040c/Source/devices/St7701Display.h +++ /dev/null @@ -1,36 +0,0 @@ -#pragma once - -#include -#include - -class St7701Display final : public EspLcdDisplay { - - std::shared_ptr touchDevice; - - 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; - - bool isRgbPanel() const override { return true; } - - lvgl_port_display_rgb_cfg_t getLvglPortDisplayRgbConfig(esp_lcd_panel_io_handle_t ioHandle, esp_lcd_panel_handle_t panelHandle) override; - -public: - - St7701Display() : EspLcdDisplay() {} - - std::string getName() const override { return "ST7701S"; } - - std::string getDescription() const override { return "ST7701S RGB display"; } - - std::shared_ptr getTouchDevice() override; - - void setBacklightDuty(uint8_t backlightDuty) override; - - bool supportsBacklightDuty() const override { return true; } - - // TODO: Find out why it crashes - bool supportsDisplayDriver() const override { return false; } -}; diff --git a/Devices/cyd-4848s040c/cyd,4848s040c.dts b/Devices/cyd-4848s040c/cyd,4848s040c.dts index 3bf2a00eb..6a5471b2f 100644 --- a/Devices/cyd-4848s040c/cyd,4848s040c.dts +++ b/Devices/cyd-4848s040c/cyd,4848s040c.dts @@ -5,8 +5,12 @@ #include #include #include -#include #include +#include +#include +#include +#include +#include / { compatible = "root"; @@ -33,5 +37,137 @@ clock-frequency = <400000>; pin-sda = <&gpio0 19 GPIO_FLAG_NONE>; pin-scl = <&gpio0 45 GPIO_FLAG_NONE>; + + touch0 { + // No reset/interrupt pin wired up here, matching the original deprecated-HAL + // config (Gt911Touch::Configuration was constructed with no reset/interrupt pin + // arguments on this board). + compatible = "goodix,gt911"; + reg = <0x5D>; + x-max = <480>; + y-max = <480>; + }; + }; + + display_backlight_pwm { + compatible = "espressif,esp32-pwm-ledc"; + pin = <&gpio0 38 GPIO_FLAG_NONE>; + period-ns = <1000000>; + ledc-timer = <0>; + ledc-channel = <0>; + }; + + display_backlight { + compatible = "pwm-backlight"; + pwm = <&display_backlight_pwm>; + }; + + display0 { + compatible = "sitronix,st7701"; + horizontal-resolution = <480>; + vertical-resolution = <480>; + pixel-clock-hz = <14000000>; + hsync-pulse-width = <10>; + hsync-back-porch = <10>; + hsync-front-porch = <20>; + vsync-pulse-width = <10>; + vsync-back-porch = <10>; + vsync-front-porch = <10>; + num-fbs = <2>; + double-fb; + bounce-buffer-size-px = <4800>; + bb-invalidate-cache; + pin-hsync = <&gpio0 16 GPIO_FLAG_NONE>; + pin-vsync = <&gpio0 17 GPIO_FLAG_NONE>; + pin-de = <&gpio0 18 GPIO_FLAG_NONE>; + pin-pclk = <&gpio0 21 GPIO_FLAG_NONE>; + pin-data0 = <&gpio0 4 GPIO_FLAG_NONE>; // B1 + pin-data1 = <&gpio0 5 GPIO_FLAG_NONE>; // B2 + pin-data2 = <&gpio0 6 GPIO_FLAG_NONE>; // B3 + pin-data3 = <&gpio0 7 GPIO_FLAG_NONE>; // B4 + pin-data4 = <&gpio0 15 GPIO_FLAG_NONE>; // B5 + pin-data5 = <&gpio0 8 GPIO_FLAG_NONE>; // G1 + pin-data6 = <&gpio0 20 GPIO_FLAG_NONE>; // G2 + pin-data7 = <&gpio0 3 GPIO_FLAG_NONE>; // G3 + pin-data8 = <&gpio0 46 GPIO_FLAG_NONE>; // G4 + pin-data9 = <&gpio0 9 GPIO_FLAG_NONE>; // G5 + pin-data10 = <&gpio0 10 GPIO_FLAG_NONE>; // G6 + pin-data11 = <&gpio0 11 GPIO_FLAG_NONE>; // R1 + pin-data12 = <&gpio0 12 GPIO_FLAG_NONE>; // R2 + pin-data13 = <&gpio0 13 GPIO_FLAG_NONE>; // R3 + pin-data14 = <&gpio0 14 GPIO_FLAG_NONE>; // R4 + pin-data15 = <&gpio0 0 GPIO_FLAG_NONE>; // R5 + + // 3-wire bit-banged command bus. GPIO47/48 are physically shared with this board's SD + // card SPI2 bus (MOSI/SCLK) - no alternate pins exist on this PCB. No SD card device is + // declared in this devicetree yet (CS/MISO pins are unverified), but if one is added on + // SPI2_HOST later, declare it *after* this node: starting that SPI bus unconditionally + // reprograms the GPIO matrix for its MOSI/SCLK pins (see esp32_spi.cpp's start()), + // which reclaims them from this display's bit-banged use without any extra code. + pin-cs = <&gpio0 39 GPIO_FLAG_NONE>; + pin-scl = <&gpio0 48 GPIO_FLAG_NONE>; + pin-sda = <&gpio0 47 GPIO_FLAG_NONE>; + mirror-by-cmd; + + init-sequence = [ + // Vendor bring-up sequence for this panel. Framing is + // [cmd, data-len, delay-ms, data-len bytes of data...] - see st7701-module's + // init-sequence binding property for the encoding. + 0xFF 5 0 0x77 0x01 0x00 0x00 0x10 + 0xC0 2 0 0x3B 0x00 + 0xC1 2 0 0x0D 0x02 + 0xC2 2 0 0x31 0x05 + 0xCD 1 0 0x00 + // Positive Voltage Gamma Control + 0xB0 16 0 0x00 0x11 0x18 0x0E 0x11 0x06 0x07 0x08 0x07 0x22 0x04 0x12 0x0F 0xAA 0x31 0x18 + // Negative Voltage Gamma Control + 0xB1 16 0 0x00 0x11 0x19 0x0E 0x12 0x07 0x08 0x08 0x08 0x22 0x04 0x11 0x11 0xA9 0x32 0x18 + // Page1 + 0xFF 5 0 0x77 0x01 0x00 0x00 0x11 + 0xB0 1 0 0x60 // Vop=4.7375v + 0xB1 1 0 0x32 // VCOM=32 + 0xB2 1 0 0x07 // VGH=15v + 0xB3 1 0 0x80 + 0xB5 1 0 0x49 // VGL=-10.17v + 0xB7 1 0 0x85 + 0xB8 1 0 0x21 // AVDD=6.6 & AVCL=-4.6 + 0xC1 1 0 0x78 + 0xC2 1 0 0x78 + 0xE0 3 0 0x00 0x1B 0x02 + 0xE1 11 0 0x08 0xA0 0x00 0x00 0x07 0xA0 0x00 0x00 0x00 0x44 0x44 + 0xE2 12 0 0x11 0x11 0x44 0x44 0xED 0xA0 0x00 0x00 0xEC 0xA0 0x00 0x00 + 0xE3 4 0 0x00 0x00 0x11 0x11 + 0xE4 2 0 0x44 0x44 + 0xE5 16 0 0x0A 0xE9 0xD8 0xA0 0x0C 0xEB 0xD8 0xA0 0x0E 0xED 0xD8 0xA0 0x10 0xEF 0xD8 0xA0 + 0xE6 4 0 0x00 0x00 0x11 0x11 + 0xE7 2 0 0x44 0x44 + 0xE8 16 0 0x09 0xE8 0xD8 0xA0 0x0B 0xEA 0xD8 0xA0 0x0D 0xEC 0xD8 0xA0 0x0F 0xEE 0xD8 0xA0 + 0xEB 7 0 0x02 0x00 0xE4 0xE4 0x88 0x00 0x40 + 0xEC 2 0 0x3C 0x00 + 0xED 16 0 0xAB 0x89 0x76 0x54 0x02 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0x20 0x45 0x67 0x98 0xBA + // VAP & VAN + 0xFF 5 0 0x77 0x01 0x00 0x00 0x13 + 0xE5 1 0 0xE4 + 0xFF 5 0 0x77 0x01 0x00 0x00 0x00 + 0x3A 1 10 0x60 // 0x70 RGB888, 0x60 RGB666, 0x50 RGB565 + 0x11 0 120 // Sleep Out + 0x29 0 0 // Display On + ]; + + backlight = <&display_backlight>; + }; + + spi0 { + compatible = "espressif,esp32-spi"; + host = ; + cs-gpios = <&gpio0 42 GPIO_FLAG_NONE>; + pin-mosi = <&gpio0 47 GPIO_FLAG_NONE>; + pin-miso = <&gpio0 41 GPIO_FLAG_NONE>; + pin-sclk = <&gpio0 48 GPIO_FLAG_NONE>; + + sdcard@0 { + compatible = "espressif,esp32-sdspi"; + frequency-khz = <20000>; + }; }; }; diff --git a/Devices/cyd-4848s040c/device.properties b/Devices/cyd-4848s040c/device.properties index 45c66c54e..1a8c9a739 100644 --- a/Devices/cyd-4848s040c/device.properties +++ b/Devices/cyd-4848s040c/device.properties @@ -10,6 +10,8 @@ hardware.spiRamMode=OCT hardware.spiRamSpeed=80M hardware.bluetooth=true +dependencies.useDeprecatedHal=false + storage.userDataLocation=SD display.size=4" diff --git a/Devices/cyd-4848s040c/devicetree.yaml b/Devices/cyd-4848s040c/devicetree.yaml index 233976646..2e9427c84 100644 --- a/Devices/cyd-4848s040c/devicetree.yaml +++ b/Devices/cyd-4848s040c/devicetree.yaml @@ -1,3 +1,5 @@ dependencies: - Platforms/platform-esp32 + - Drivers/st7701-module + - Drivers/gt911-module dts: cyd,4848s040c.dts diff --git a/Devices/cyd-4848s040c/Source/module.cpp b/Devices/cyd-4848s040c/source/module.cpp similarity index 78% rename from Devices/cyd-4848s040c/Source/module.cpp rename to Devices/cyd-4848s040c/source/module.cpp index 6ec9afb28..d30b725e3 100644 --- a/Devices/cyd-4848s040c/Source/module.cpp +++ b/Devices/cyd-4848s040c/source/module.cpp @@ -3,16 +3,14 @@ extern "C" { static error_t start() { - // Empty for now return ERROR_NONE; } static error_t stop() { - // Empty for now return ERROR_NONE; } -Module cyd_4848s040c_module = { +struct Module cyd_4848s040c_module = { .name = "cyd-4848s040c", .start = start, .stop = stop, diff --git a/Devices/guition-jc1060p470ciwy/CMakeLists.txt b/Devices/guition-jc1060p470ciwy/CMakeLists.txt index d90d55071..a7d3e6712 100644 --- a/Devices/guition-jc1060p470ciwy/CMakeLists.txt +++ b/Devices/guition-jc1060p470ciwy/CMakeLists.txt @@ -1,8 +1,6 @@ -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 EspLcdCompat esp_lcd_jd9165 GT911 PwmBacklight driver vfs fatfs - PRIV_REQUIRES esp_adc EstimatedPower + REQUIRES TactilityKernel ) diff --git a/Devices/guition-jc1060p470ciwy/Source/Configuration.cpp b/Devices/guition-jc1060p470ciwy/Source/Configuration.cpp deleted file mode 100644 index ab3a805f2..000000000 --- a/Devices/guition-jc1060p470ciwy/Source/Configuration.cpp +++ /dev/null @@ -1,17 +0,0 @@ -#include "devices/Display.h" -#include "devices/Power.h" - -#include - -using namespace tt::hal; - -static DeviceVector createDevices() { - return { - createDisplay(), - createPower() - }; -} - -extern const Configuration hardwareConfiguration = { - .createDevices = createDevices, -}; diff --git a/Devices/guition-jc1060p470ciwy/Source/devices/Display.cpp b/Devices/guition-jc1060p470ciwy/Source/devices/Display.cpp deleted file mode 100644 index d77689c5b..000000000 --- a/Devices/guition-jc1060p470ciwy/Source/devices/Display.cpp +++ /dev/null @@ -1,66 +0,0 @@ -#include "Display.h" -#include "Jd9165Display.h" - -#include -#include -#include -#include -#include -#include - -constexpr auto LCD_PIN_RESET = GPIO_NUM_0; // Match P4 EV board reset line -constexpr auto LCD_PIN_BACKLIGHT = GPIO_NUM_23; -constexpr auto LCD_HORIZONTAL_RESOLUTION = 1024; -constexpr auto LCD_VERTICAL_RESOLUTION = 600; - -constexpr auto TOUCH_PIN_RESET = GPIO_NUM_NC; -constexpr auto TOUCH_PIN_INTERRUPT = GPIO_NUM_NC; - -static std::shared_ptr createTouch() { - auto* i2c = device_find_by_name("i2c_internal"); - check(i2c); - auto configuration = std::make_unique( - i2c, - LCD_HORIZONTAL_RESOLUTION, - LCD_VERTICAL_RESOLUTION, - false, // swapXY - false, // mirrorX - false, // mirrorY - TOUCH_PIN_RESET, - TOUCH_PIN_INTERRUPT - ); - - return std::make_shared(std::move(configuration)); -} - -std::shared_ptr createDisplay() { - // Initialize PWM backlight - if (!driver::pwmbacklight::init(LCD_PIN_BACKLIGHT, 20000, LEDC_TIMER_1, LEDC_CHANNEL_0)) { - LOG_W("jc1060p470ciwy", "Failed to initialize backlight"); - } - - auto touch = createTouch(); - - auto configuration = std::make_shared(EspLcdConfiguration { - .horizontalResolution = LCD_HORIZONTAL_RESOLUTION, - .verticalResolution = LCD_VERTICAL_RESOLUTION, - .gapX = 0, - .gapY = 0, - .monochrome = false, - .swapXY = false, - .mirrorX = false, - .mirrorY = false, - .invertColor = false, - .bufferSize = 0, // 0 = default (1/10 of screen) - .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 - }); - - const auto display = std::make_shared(configuration); - return std::static_pointer_cast(display); -} diff --git a/Devices/guition-jc1060p470ciwy/Source/devices/Jd9165Display.cpp b/Devices/guition-jc1060p470ciwy/Source/devices/Jd9165Display.cpp deleted file mode 100644 index 0451355ec..000000000 --- a/Devices/guition-jc1060p470ciwy/Source/devices/Jd9165Display.cpp +++ /dev/null @@ -1,203 +0,0 @@ -#include "Jd9165Display.h" - -#include - -#include - -constexpr auto* TAG = "JD9165"; - -// MIPI DSI PHY power configuration -#define MIPI_DSI_PHY_PWR_LDO_CHAN 3 // LDO_VO3 connects to VDD_MIPI_DPHY -#define MIPI_DSI_PHY_PWR_LDO_VOLTAGE_MV 2500 - -// JD9165 initialization commands from ESP32-P4 Function EV Board -// Delays set to match the reference sequence exactly. -static const jd9165_lcd_init_cmd_t jd9165_init_cmds[] = { - {0x30, (uint8_t[]){0x00}, 1, 0}, - {0xF7, (uint8_t[]){0x49,0x61,0x02,0x00}, 4, 0}, - {0x30, (uint8_t[]){0x01}, 1, 0}, - {0x04, (uint8_t[]){0x0C}, 1, 0}, - {0x05, (uint8_t[]){0x00}, 1, 0}, - {0x06, (uint8_t[]){0x00}, 1, 0}, - {0x0B, (uint8_t[]){0x11}, 1, 0}, - {0x17, (uint8_t[]){0x00}, 1, 0}, - {0x20, (uint8_t[]){0x04}, 1, 0}, - {0x1F, (uint8_t[]){0x05}, 1, 0}, - {0x23, (uint8_t[]){0x00}, 1, 0}, - {0x25, (uint8_t[]){0x19}, 1, 0}, - {0x28, (uint8_t[]){0x18}, 1, 0}, - {0x29, (uint8_t[]){0x04}, 1, 0}, - {0x2A, (uint8_t[]){0x01}, 1, 0}, - {0x2B, (uint8_t[]){0x04}, 1, 0}, - {0x2C, (uint8_t[]){0x01}, 1, 0}, - {0x30, (uint8_t[]){0x02}, 1, 0}, - {0x01, (uint8_t[]){0x22}, 1, 0}, - {0x03, (uint8_t[]){0x12}, 1, 0}, - {0x04, (uint8_t[]){0x00}, 1, 0}, - {0x05, (uint8_t[]){0x64}, 1, 0}, - {0x0A, (uint8_t[]){0x08}, 1, 0}, - {0x0B, (uint8_t[]){0x0A,0x1A,0x0B,0x0D,0x0D,0x11,0x10,0x06,0x08,0x1F,0x1D}, 11, 0}, - {0x0C, (uint8_t[]){0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D}, 11, 0}, - {0x0D, (uint8_t[]){0x16,0x1B,0x0B,0x0D,0x0D,0x11,0x10,0x07,0x09,0x1E,0x1C}, 11, 0}, - {0x0E, (uint8_t[]){0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D}, 11, 0}, - {0x0F, (uint8_t[]){0x16,0x1B,0x0D,0x0B,0x0D,0x11,0x10,0x1C,0x1E,0x09,0x07}, 11, 0}, - {0x10, (uint8_t[]){0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D}, 11, 0}, - {0x11, (uint8_t[]){0x0A,0x1A,0x0D,0x0B,0x0D,0x11,0x10,0x1D,0x1F,0x08,0x06}, 11, 0}, - {0x12, (uint8_t[]){0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D}, 11, 0}, - {0x14, (uint8_t[]){0x00,0x00,0x11,0x11}, 4, 0}, - {0x18, (uint8_t[]){0x99}, 1, 0}, - {0x30, (uint8_t[]){0x06}, 1, 0}, - {0x12, (uint8_t[]){0x36,0x2C,0x2E,0x3C,0x38,0x35,0x35,0x32,0x2E,0x1D,0x2B,0x21,0x16,0x29}, 14, 0}, - {0x13, (uint8_t[]){0x36,0x2C,0x2E,0x3C,0x38,0x35,0x35,0x32,0x2E,0x1D,0x2B,0x21,0x16,0x29}, 14, 0}, - {0x30, (uint8_t[]){0x0A}, 1, 0}, - {0x02, (uint8_t[]){0x4F}, 1, 0}, - {0x0B, (uint8_t[]){0x40}, 1, 0}, - {0x12, (uint8_t[]){0x3E}, 1, 0}, - {0x13, (uint8_t[]){0x78}, 1, 0}, - {0x30, (uint8_t[]){0x0D}, 1, 0}, - {0x0D, (uint8_t[]){0x04}, 1, 0}, - {0x10, (uint8_t[]){0x0C}, 1, 0}, - {0x11, (uint8_t[]){0x0C}, 1, 0}, - {0x12, (uint8_t[]){0x0C}, 1, 0}, - {0x13, (uint8_t[]){0x0C}, 1, 0}, - {0x30, (uint8_t[]){0x00}, 1, 0}, - {0X3A, (uint8_t[]){0x55}, 1, 0}, - {0x11, (uint8_t[]){0x00}, 1, 120}, - {0x29, (uint8_t[]){0x00}, 1, 20}, -}; - -Jd9165Display::~Jd9165Display() { - // 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 Jd9165Display::createMipiDsiBus() { - // Enable MIPI DSI PHY power (transition from "no power" to "shutdown" state) - esp_ldo_channel_config_t ldo_mipi_phy_config = { - .chan_id = MIPI_DSI_PHY_PWR_LDO_CHAN, - .voltage_mv = MIPI_DSI_PHY_PWR_LDO_VOLTAGE_MV, - .flags = {} - }; - - 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, "MIPI DSI PHY powered on"); - - // Create MIPI DSI 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 = 750 - }; - - if (esp_lcd_new_dsi_bus(&bus_config, &mipiDsiBus) != ESP_OK) { - LOG_E(TAG, "Failed to create MIPI DSI bus"); - return false; - } - - LOG_I(TAG, "MIPI DSI bus created"); - return true; -} - -bool Jd9165Display::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 = JD9165_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"); - return false; - } - - return true; -} - -esp_lcd_panel_dev_config_t Jd9165Display::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 Jd9165Display::createPanelHandle(esp_lcd_panel_io_handle_t ioHandle, const esp_lcd_panel_dev_config_t& panelConfig, esp_lcd_panel_handle_t& panelHandle) { - // Create DPI panel configuration - // Override default timings - 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 = 50, - .pixel_format = LCD_COLOR_PIXEL_FORMAT_RGB565, - .in_color_format = LCD_COLOR_FMT_RGB565, - .out_color_format = LCD_COLOR_FMT_RGB565, - .num_fbs = 1, - .video_timing = { - .h_size = 1024, - .v_size = 600, - .hsync_pulse_width = 20, - .hsync_back_porch = 160, - .hsync_front_porch = 160, - .vsync_pulse_width = 2, - .vsync_back_porch = 21, - .vsync_front_porch = 12, - }, - .flags = { - .use_dma2d = 1, - .disable_lp = 0 - } - }; - - jd9165_vendor_config_t vendor_config = { - .init_cmds = jd9165_init_cmds, - .init_cmds_size = sizeof(jd9165_init_cmds) / sizeof(jd9165_lcd_init_cmd_t), - .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_jd9165(ioHandle, &mutable_panel_config, &panelHandle) != ESP_OK) { - LOG_E(TAG, "Failed to create panel"); - return false; - } - - LOG_I(TAG, "JD9165 panel created successfully"); - // Defer reset/init to base class applyConfiguration to avoid double initialization - return true; -} - -lvgl_port_display_dsi_cfg_t Jd9165Display::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/guition-jc1060p470ciwy/Source/devices/Jd9165Display.h b/Devices/guition-jc1060p470ciwy/Source/devices/Jd9165Display.h deleted file mode 100644 index 98d33c6a8..000000000 --- a/Devices/guition-jc1060p470ciwy/Source/devices/Jd9165Display.h +++ /dev/null @@ -1,39 +0,0 @@ -#pragma once - -#include -#include - -#include -#include - -class Jd9165Display 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: - - Jd9165Display( - const std::shared_ptr& configuration - ) : EspLcdDisplayV2(configuration) {} - - ~Jd9165Display() override; - - std::string getName() const override { return "JD9165"; } - - std::string getDescription() const override { return "JD9165 MIPI-DSI 1024x600 display"; } -}; diff --git a/Devices/guition-jc1060p470ciwy/Source/devices/Power.cpp b/Devices/guition-jc1060p470ciwy/Source/devices/Power.cpp deleted file mode 100644 index e221f92db..000000000 --- a/Devices/guition-jc1060p470ciwy/Source/devices/Power.cpp +++ /dev/null @@ -1,180 +0,0 @@ -#include "Power.h" - -#include -#include -#include -#include -#include - -using tt::hal::power::PowerDevice; - -constexpr auto* TAG = "JcPower"; - -namespace { - -constexpr adc_unit_t ADC_UNIT = ADC_UNIT_2; -constexpr adc_channel_t ADC_CHANNEL = ADC_CHANNEL_4; // matches ADC2 CH4 used in brookesia config -constexpr adc_atten_t ADC_ATTEN = ADC_ATTEN_DB_12; -constexpr int32_t UPPER_RESISTOR_OHM = 85'000; // per brookesia settings -constexpr int32_t LOWER_RESISTOR_OHM = 100'000; // per brookesia settings - -class JcPower final : public PowerDevice { -public: - JcPower() : chargeEstimator(3.3f, 4.2f) {} - ~JcPower() override { deinit(); } - - std::string getName() const override { return "JC Power"; } - std::string getDescription() const override { return "Battery voltage via ADC"; } - - bool supportsMetric(MetricType type) const override { - switch (type) { - using enum MetricType; - case BatteryVoltage: - case ChargeLevel: - return true; - default: - return false; - } - } - - bool getMetric(MetricType type, MetricData& data) override { - if (!ensureInit()) { - return false; - } - - uint32_t batteryMv = 0; - if (!readBatteryMilliVolt(batteryMv)) { - return false; - } - - switch (type) { - case MetricType::BatteryVoltage: - data.valueAsUint32 = batteryMv; - return true; - case MetricType::ChargeLevel: - data.valueAsUint8 = chargeEstimator.estimateCharge(batteryMv); - return true; - default: - return false; - } - } - -private: - bool ensureInit() { - if (initialized) { - return true; - } - - adc_oneshot_unit_init_cfg_t init_cfg = { - .unit_id = ADC_UNIT, - .clk_src = ADC_RTC_CLK_SRC_DEFAULT, - .ulp_mode = ADC_ULP_MODE_DISABLE, - }; - if (adc_oneshot_new_unit(&init_cfg, &adcHandle) != ESP_OK) { - LOG_E(TAG, "ADC unit init failed"); - return false; - } - - adc_oneshot_chan_cfg_t chan_cfg = { - .atten = ADC_ATTEN, - .bitwidth = ADC_BITWIDTH_DEFAULT, - }; - if (adc_oneshot_config_channel(adcHandle, ADC_CHANNEL, &chan_cfg) != ESP_OK) { - LOG_E(TAG, "ADC channel config failed"); - adc_oneshot_del_unit(adcHandle); - adcHandle = nullptr; - return false; - } - - calibrated = tryInitCalibration(); - initialized = true; - return true; - } - - bool tryInitCalibration() { -#if ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED - adc_cali_line_fitting_config_t cali_config = { - .unit_id = ADC_UNIT, - .atten = ADC_ATTEN, - .bitwidth = ADC_BITWIDTH_DEFAULT, - }; - if (adc_cali_create_scheme_line_fitting(&cali_config, &caliHandle) == ESP_OK) { - calScheme = CaliScheme::Line; - LOG_I(TAG, "ADC calibration (line fitting) enabled"); - return true; - } -#endif - -#if ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED - adc_cali_curve_fitting_config_t curve_cfg = { - .unit_id = ADC_UNIT, - .chan = ADC_CHANNEL, - .atten = ADC_ATTEN, - .bitwidth = ADC_BITWIDTH_DEFAULT, - }; - if (adc_cali_create_scheme_curve_fitting(&curve_cfg, &caliHandle) == ESP_OK) { - calScheme = CaliScheme::Curve; - LOG_I(TAG, "ADC calibration (curve fitting) enabled"); - return true; - } -#endif - - LOG_W(TAG, "ADC calibration not available, using raw scaling"); - return false; - } - - bool readBatteryMilliVolt(uint32_t& outMv) { - int raw = 0; - if (adc_oneshot_read(adcHandle, ADC_CHANNEL, &raw) != ESP_OK) { - LOG_E(TAG, "ADC read failed"); - return false; - } - - int mv = 0; - if (calibrated && adc_cali_raw_to_voltage(caliHandle, raw, &mv) == ESP_OK) { - // ok - } else { - // Fallback: approximate assuming 12-bit full scale 3.3V - mv = (raw * 3300) / 4095; - } - - const int64_t numerator = static_cast(UPPER_RESISTOR_OHM + LOWER_RESISTOR_OHM) * mv; - const int64_t denominator = LOWER_RESISTOR_OHM; - outMv = static_cast(numerator / denominator); - return true; - } - - void deinit() { - if (adcHandle) { - adc_oneshot_del_unit(adcHandle); - adcHandle = nullptr; - } - if (caliHandle) { - if (calScheme == CaliScheme::Line) { -#if ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED - adc_cali_delete_scheme_line_fitting(caliHandle); -#endif -#if ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED - } else if (calScheme == CaliScheme::Curve) { - adc_cali_delete_scheme_curve_fitting(caliHandle); -#endif - } - caliHandle = nullptr; - calibrated = false; - } - } - - enum class CaliScheme { None, Line, Curve }; - - bool initialized = false; - bool calibrated = false; - CaliScheme calScheme = CaliScheme::None; - adc_oneshot_unit_handle_t adcHandle = nullptr; - adc_cali_handle_t caliHandle = nullptr; - ChargeFromVoltage chargeEstimator; -}; -} // namespace - -std::shared_ptr createPower() { - return std::make_shared(); -} diff --git a/Devices/guition-jc1060p470ciwy/Source/devices/Power.h b/Devices/guition-jc1060p470ciwy/Source/devices/Power.h deleted file mode 100644 index 111f0a081..000000000 --- a/Devices/guition-jc1060p470ciwy/Source/devices/Power.h +++ /dev/null @@ -1,7 +0,0 @@ -#pragma once - -#include -#include - -// Battery measurement via ADC2 channel 4 with 85k/100k divider -std::shared_ptr createPower(); diff --git a/Devices/guition-jc1060p470ciwy/device.properties b/Devices/guition-jc1060p470ciwy/device.properties index fcc7421ee..6f4a69087 100644 --- a/Devices/guition-jc1060p470ciwy/device.properties +++ b/Devices/guition-jc1060p470ciwy/device.properties @@ -11,6 +11,8 @@ hardware.spiRamSpeed=200M hardware.esptoolFlashFreq=80M hardware.bluetooth=true +dependencies.useDeprecatedHal=false + storage.userDataLocation=SD display.size=7" diff --git a/Devices/guition-jc1060p470ciwy/devicetree.yaml b/Devices/guition-jc1060p470ciwy/devicetree.yaml index 199ba532d..0f53f3dc8 100644 --- a/Devices/guition-jc1060p470ciwy/devicetree.yaml +++ b/Devices/guition-jc1060p470ciwy/devicetree.yaml @@ -1,3 +1,5 @@ dependencies: - Platforms/platform-esp32 + - Drivers/jd9165-module + - Drivers/gt911-module dts: guition,jc1060p470ciwy.dts diff --git a/Devices/guition-jc1060p470ciwy/guition,jc1060p470ciwy.dts b/Devices/guition-jc1060p470ciwy/guition,jc1060p470ciwy.dts index 67611202d..3bd33fa25 100644 --- a/Devices/guition-jc1060p470ciwy/guition,jc1060p470ciwy.dts +++ b/Devices/guition-jc1060p470ciwy/guition,jc1060p470ciwy.dts @@ -7,13 +7,20 @@ #include #include #include +#include +#include +#include +#include +#include +#include /** * For future reference: * - ES8311 on I2C with PA PIN at GPIO 11 * - Built-in led at GPIO 26 * - Boot button at GPIO 21 - * - LCD reset: GPIO 27 + * - LCD reset: GPIO 0 (matches P4 EV board reset line - deprecated HAL's old Display.cpp + * comment claimed GPIO 27 here, but its actual LCD_PIN_RESET constant was GPIO_NUM_0) * - LCD backlight: GPIO 23 */ / { @@ -41,6 +48,13 @@ clock-frequency = <400000>; pin-sda = <&gpio0 7 GPIO_FLAG_NONE>; pin-scl = <&gpio0 8 GPIO_FLAG_NONE>; + + touch0 { + compatible = "goodix,gt911"; + reg = <0x5D>; + x-max = <1024>; + y-max = <600>; + }; }; i2s0 { @@ -65,4 +79,116 @@ bus-width = <4>; on-chip-ldo-chan = <4>; }; + + adc0 { + compatible = "espressif,esp32-adc-oneshot"; + unit-id = ; + channels = ; + }; + + // Matches the deprecated HAL's old Power.cpp: ADC2 CH4 behind an 85k/100k divider + // (multiplier = (85000+100000)/100000 = 1.850), reference-voltage-mv is the nominal + // full-scale value at ADC_ATTEN_DB_12 (battery-sense has no calibration path, unlike the + // old driver's adc_cali fallback). + battery-sense { + compatible = "battery-sense"; + io-channel = <&adc0 0>; + reference-voltage-mv = <3300>; + multiplier = <1850>; + }; + + display_backlight_pwm { + compatible = "espressif,esp32-pwm-ledc"; + pin = <&gpio0 23 GPIO_FLAG_NONE>; + period-ns = <50000>; + ledc-timer = <1>; + ledc-channel = <0>; + }; + + display_backlight { + compatible = "pwm-backlight"; + pwm = <&display_backlight_pwm>; + }; + + display0 { + compatible = "jdi,jd9165"; + horizontal-resolution = <1024>; + vertical-resolution = <600>; + pin-reset = <&gpio0 0 GPIO_FLAG_NONE>; + ldo-channel = <3>; // LDO_VO3 connects to VDD_MIPI_DPHY + ldo-voltage-mv = <2500>; + num-data-lanes = <2>; + lane-bit-rate-mbps = <750>; + dpi-clock-freq-mhz = <50>; + hsync-pulse-width = <20>; + hsync-back-porch = <160>; + hsync-front-porch = <160>; + vsync-pulse-width = <2>; + vsync-back-porch = <21>; + vsync-front-porch = <12>; + // Skips the wait-for-scanout in draw_bitmap(): prevents stalls/blank flashes when + // other tasks (e.g. flash writes) block timing, at the cost of tear-free rendering + // (matches the deprecated HAL's old Jd9165Display::getLvglPortDisplayDsiConfig(), + // which disabled esp_lvgl_port's avoid_tearing for the same reason). + allow-tearing; + backlight = <&display_backlight>; + + // Vendor bring-up sequence from the ESP32-P4 Function EV Board reference, carried over + // unchanged from the deprecated HAL's old Jd9165Display.cpp. Framing is + // [cmd, data-len, delay-ms, data-len bytes of data...] - see jd9165-module's + // init-sequence binding property for the encoding. + init-sequence = [ + 0x30 1 0 0x00 + 0xF7 4 0 0x49 0x61 0x02 0x00 + 0x30 1 0 0x01 + 0x04 1 0 0x0C + 0x05 1 0 0x00 + 0x06 1 0 0x00 + 0x0B 1 0 0x11 + 0x17 1 0 0x00 + 0x20 1 0 0x04 + 0x1F 1 0 0x05 + 0x23 1 0 0x00 + 0x25 1 0 0x19 + 0x28 1 0 0x18 + 0x29 1 0 0x04 + 0x2A 1 0 0x01 + 0x2B 1 0 0x04 + 0x2C 1 0 0x01 + 0x30 1 0 0x02 + 0x01 1 0 0x22 + 0x03 1 0 0x12 + 0x04 1 0 0x00 + 0x05 1 0 0x64 + 0x0A 1 0 0x08 + 0x0B 11 0 0x0A 0x1A 0x0B 0x0D 0x0D 0x11 0x10 0x06 0x08 0x1F 0x1D + 0x0C 11 0 0x0D 0x0D 0x0D 0x0D 0x0D 0x0D 0x0D 0x0D 0x0D 0x0D 0x0D + 0x0D 11 0 0x16 0x1B 0x0B 0x0D 0x0D 0x11 0x10 0x07 0x09 0x1E 0x1C + 0x0E 11 0 0x0D 0x0D 0x0D 0x0D 0x0D 0x0D 0x0D 0x0D 0x0D 0x0D 0x0D + 0x0F 11 0 0x16 0x1B 0x0D 0x0B 0x0D 0x11 0x10 0x1C 0x1E 0x09 0x07 + 0x10 11 0 0x0D 0x0D 0x0D 0x0D 0x0D 0x0D 0x0D 0x0D 0x0D 0x0D 0x0D + 0x11 11 0 0x0A 0x1A 0x0D 0x0B 0x0D 0x11 0x10 0x1D 0x1F 0x08 0x06 + 0x12 11 0 0x0D 0x0D 0x0D 0x0D 0x0D 0x0D 0x0D 0x0D 0x0D 0x0D 0x0D + 0x14 4 0 0x00 0x00 0x11 0x11 + 0x18 1 0 0x99 + 0x30 1 0 0x06 + 0x12 14 0 0x36 0x2C 0x2E 0x3C 0x38 0x35 0x35 0x32 0x2E 0x1D 0x2B 0x21 0x16 0x29 + 0x13 14 0 0x36 0x2C 0x2E 0x3C 0x38 0x35 0x35 0x32 0x2E 0x1D 0x2B 0x21 0x16 0x29 + 0x30 1 0 0x0A + 0x02 1 0 0x4F + 0x0B 1 0 0x40 + 0x12 1 0 0x3E + 0x13 1 0 0x78 + 0x30 1 0 0x0D + 0x0D 1 0 0x04 + 0x10 1 0 0x0C + 0x11 1 0 0x0C + 0x12 1 0 0x0C + 0x13 1 0 0x0C + 0x30 1 0 0x00 + 0x3A 1 0 0x55 + 0x11 1 120 0x00 + 0x29 1 20 0x00 + ]; + }; }; diff --git a/Devices/guition-jc1060p470ciwy/Source/module.cpp b/Devices/guition-jc1060p470ciwy/source/module.cpp similarity index 75% rename from Devices/guition-jc1060p470ciwy/Source/module.cpp rename to Devices/guition-jc1060p470ciwy/source/module.cpp index efecf6632..789fcff00 100644 --- a/Devices/guition-jc1060p470ciwy/Source/module.cpp +++ b/Devices/guition-jc1060p470ciwy/source/module.cpp @@ -3,16 +3,14 @@ extern "C" { static error_t start() { - // Empty for now return ERROR_NONE; } static error_t stop() { - // Empty for now return ERROR_NONE; } -struct Module guition_jc1060p470ciwy_module = { +Module guition_jc1060p470ciwy_module = { .name = "guition-jc1060p470ciwy", .start = start, .stop = stop, diff --git a/Devices/guition-jc2432w328c/CMakeLists.txt b/Devices/guition-jc2432w328c/CMakeLists.txt index 38845d132..a832a83a2 100644 --- a/Devices/guition-jc2432w328c/CMakeLists.txt +++ b/Devices/guition-jc2432w328c/CMakeLists.txt @@ -1,7 +1,6 @@ -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 ST7789 CST816S PwmBacklight driver vfs fatfs + REQUIRES TactilityKernel driver ) diff --git a/Devices/guition-jc2432w328c/Source/Configuration.cpp b/Devices/guition-jc2432w328c/Source/Configuration.cpp deleted file mode 100644 index 8324dcffe..000000000 --- a/Devices/guition-jc2432w328c/Source/Configuration.cpp +++ /dev/null @@ -1,21 +0,0 @@ -#include "devices/Display.h" - -#include -#include - -using namespace tt::hal; - -static bool initBoot() { - return driver::pwmbacklight::init(LCD_PIN_BACKLIGHT); -} - -static DeviceVector createDevices() { - return { - createDisplay(), - }; -} - -extern const Configuration hardwareConfiguration = { - .initBoot = initBoot, - .createDevices = createDevices -}; diff --git a/Devices/guition-jc2432w328c/Source/devices/Display.cpp b/Devices/guition-jc2432w328c/Source/devices/Display.cpp deleted file mode 100644 index 1cb7d1895..000000000 --- a/Devices/guition-jc2432w328c/Source/devices/Display.cpp +++ /dev/null @@ -1,43 +0,0 @@ -#include "Display.h" - -#include -#include -#include - -static std::shared_ptr createTouch() { - auto configuration = std::make_unique( - I2C_NUM_0, - LCD_HORIZONTAL_RESOLUTION, - LCD_VERTICAL_RESOLUTION - ); - - return std::make_shared(std::move(configuration)); -} - -std::shared_ptr createDisplay() { - St7789Display::Configuration panel_configuration = { - .horizontalResolution = LCD_HORIZONTAL_RESOLUTION, - .verticalResolution = LCD_VERTICAL_RESOLUTION, - .gapX = 0, - .gapY = 0, - .swapXY = false, - .mirrorX = false, - .mirrorY = false, - .invertColor = false, - .bufferSize = LCD_BUFFER_SIZE, - .touch = createTouch(), - .backlightDutyFunction = driver::pwmbacklight::setBacklightDuty, - .resetPin = GPIO_NUM_NC, - .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/guition-jc2432w328c/Source/devices/Display.h b/Devices/guition-jc2432w328c/Source/devices/Display.h deleted file mode 100644 index 26e2214a8..000000000 --- a/Devices/guition-jc2432w328c/Source/devices/Display.h +++ /dev/null @@ -1,22 +0,0 @@ -#pragma once - -#include "Display.h" - -#include -#include -#include -#include - -// Display backlight (PWM) -constexpr auto LCD_PIN_BACKLIGHT = GPIO_NUM_27; - -// Display -constexpr auto LCD_SPI_HOST = SPI2_HOST; -constexpr auto LCD_PIN_CS = GPIO_NUM_15; -constexpr auto LCD_PIN_DC = GPIO_NUM_2; -constexpr auto LCD_HORIZONTAL_RESOLUTION = 240; -constexpr auto LCD_VERTICAL_RESOLUTION = 320; -constexpr auto LCD_BUFFER_HEIGHT = LCD_VERTICAL_RESOLUTION / 10; -constexpr auto LCD_BUFFER_SIZE = LCD_HORIZONTAL_RESOLUTION * LCD_BUFFER_HEIGHT; - -std::shared_ptr createDisplay(); diff --git a/Devices/guition-jc2432w328c/device.properties b/Devices/guition-jc2432w328c/device.properties index 7ef40633b..f0d9ead2d 100644 --- a/Devices/guition-jc2432w328c/device.properties +++ b/Devices/guition-jc2432w328c/device.properties @@ -7,6 +7,8 @@ hardware.target=ESP32 hardware.flashSize=4MB hardware.spiRam=false +dependencies.useDeprecatedHal=false + storage.userDataLocation=SD display.size=2.8" diff --git a/Devices/guition-jc2432w328c/devicetree.yaml b/Devices/guition-jc2432w328c/devicetree.yaml index ca67575e7..90e0fb90b 100644 --- a/Devices/guition-jc2432w328c/devicetree.yaml +++ b/Devices/guition-jc2432w328c/devicetree.yaml @@ -1,3 +1,5 @@ dependencies: - Platforms/platform-esp32 + - Drivers/st7789-module + - Drivers/cst816s-module dts: guition,jc2432w328c.dts diff --git a/Devices/guition-jc2432w328c/guition,jc2432w328c.dts b/Devices/guition-jc2432w328c/guition,jc2432w328c.dts index acad47f5f..138df637c 100644 --- a/Devices/guition-jc2432w328c/guition,jc2432w328c.dts +++ b/Devices/guition-jc2432w328c/guition,jc2432w328c.dts @@ -8,8 +8,10 @@ #include #include #include -#include +#include #include +#include +#include / { compatible = "root"; @@ -31,6 +33,13 @@ clock-frequency = <400000>; pin-sda = <&gpio0 33 GPIO_FLAG_NONE>; pin-scl = <&gpio0 32 GPIO_FLAG_NONE>; + + touch { + compatible = "hynitron,cst816s"; + reg = <0x15>; + x-max = <240>; + y-max = <320>; + }; }; // CN1 header @@ -42,15 +51,36 @@ pin-scl = <&gpio0 22 GPIO_FLAG_NONE>; }; + display_backlight_pwm { + compatible = "espressif,esp32-pwm-ledc"; + pin = <&gpio0 27 GPIO_FLAG_NONE>; + period-ns = <25000>; + ledc-timer = <1>; + ledc-channel = <3>; + }; + + 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 15 GPIO_FLAG_NONE>; pin-mosi = <&gpio0 13 GPIO_FLAG_NONE>; pin-sclk = <&gpio0 14 GPIO_FLAG_NONE>; - - display { - compatible = "display-placeholder"; + + display@0 { + compatible = "sitronix,st7789"; + horizontal-resolution = <240>; + vertical-resolution = <320>; + pixel-clock-hz = <62500000>; + pin-dc = <&gpio0 2 GPIO_FLAG_NONE>; + backlight = <&display_backlight>; }; }; diff --git a/Devices/guition-jc2432w328c/Source/module.cpp b/Devices/guition-jc2432w328c/source/module.cpp similarity index 100% rename from Devices/guition-jc2432w328c/Source/module.cpp rename to Devices/guition-jc2432w328c/source/module.cpp diff --git a/Devices/guition-jc3248w535c/CMakeLists.txt b/Devices/guition-jc3248w535c/CMakeLists.txt index 933587db6..a832a83a2 100644 --- a/Devices/guition-jc3248w535c/CMakeLists.txt +++ b/Devices/guition-jc3248w535c/CMakeLists.txt @@ -1,7 +1,6 @@ -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 EspLcdCompat esp_lcd_axs15231b PwmBacklight driver vfs fatfs + REQUIRES TactilityKernel driver ) diff --git a/Devices/guition-jc3248w535c/Source/Axs15231b/Axs15231bDisplay.cpp b/Devices/guition-jc3248w535c/Source/Axs15231b/Axs15231bDisplay.cpp deleted file mode 100644 index c358ad014..000000000 --- a/Devices/guition-jc3248w535c/Source/Axs15231b/Axs15231bDisplay.cpp +++ /dev/null @@ -1,493 +0,0 @@ -#include "Axs15231bDisplay.h" - -#include -#include -#include - -#include -#include -#include -#include -#include - -constexpr auto* TAG = "AXS15231B"; - -static const axs15231b_lcd_init_cmd_t lcd_init_cmds[] = { - {0xBB, (uint8_t[]){0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5A, 0xA5}, 8, 0}, - {0xA0, (uint8_t[]){0xC0, 0x10, 0x00, 0x02, 0x00, 0x00, 0x04, 0x3F, 0x20, 0x05, 0x3F, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00}, 17, 0}, - {0xA2, (uint8_t[]){0x30, 0x3C, 0x24, 0x14, 0xD0, 0x20, 0xFF, 0xE0, 0x40, 0x19, 0x80, 0x80, 0x80, 0x20, 0xf9, 0x10, 0x02, 0xff, 0xff, 0xF0, 0x90, 0x01, 0x32, 0xA0, 0x91, 0xE0, 0x20, 0x7F, 0xFF, 0x00, 0x5A}, 31, 0}, - {0xD0, (uint8_t[]){0xE0, 0x40, 0x51, 0x24, 0x08, 0x05, 0x10, 0x01, 0x20, 0x15, 0x42, 0xC2, 0x22, 0x22, 0xAA, 0x03, 0x10, 0x12, 0x60, 0x14, 0x1E, 0x51, 0x15, 0x00, 0x8A, 0x20, 0x00, 0x03, 0x3A, 0x12}, 30, 0}, - {0xA3, (uint8_t[]){0xA0, 0x06, 0xAa, 0x00, 0x08, 0x02, 0x0A, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x00, 0x55, 0x55}, 22, 0}, - {0xC1, (uint8_t[]){0x31, 0x04, 0x02, 0x02, 0x71, 0x05, 0x24, 0x55, 0x02, 0x00, 0x41, 0x00, 0x53, 0xFF, 0xFF, 0xFF, 0x4F, 0x52, 0x00, 0x4F, 0x52, 0x00, 0x45, 0x3B, 0x0B, 0x02, 0x0d, 0x00, 0xFF, 0x40}, 30, 0}, - {0xC3, (uint8_t[]){0x00, 0x00, 0x00, 0x50, 0x03, 0x00, 0x00, 0x00, 0x01, 0x80, 0x01}, 11, 0}, - {0xC4, (uint8_t[]){0x00, 0x24, 0x33, 0x80, 0x00, 0xea, 0x64, 0x32, 0xC8, 0x64, 0xC8, 0x32, 0x90, 0x90, 0x11, 0x06, 0xDC, 0xFA, 0x00, 0x00, 0x80, 0xFE, 0x10, 0x10, 0x00, 0x0A, 0x0A, 0x44, 0x50}, 29, 0}, - {0xC5, (uint8_t[]){0x18, 0x00, 0x00, 0x03, 0xFE, 0x3A, 0x4A, 0x20, 0x30, 0x10, 0x88, 0xDE, 0x0D, 0x08, 0x0F, 0x0F, 0x01, 0x3A, 0x4A, 0x20, 0x10, 0x10, 0x00}, 23, 0}, - {0xC6, (uint8_t[]){0x05, 0x0A, 0x05, 0x0A, 0x00, 0xE0, 0x2E, 0x0B, 0x12, 0x22, 0x12, 0x22, 0x01, 0x03, 0x00, 0x3F, 0x6A, 0x18, 0xC8, 0x22}, 20, 0}, - {0xC7, (uint8_t[]){0x50, 0x32, 0x28, 0x00, 0xa2, 0x80, 0x8f, 0x00, 0x80, 0xff, 0x07, 0x11, 0x9c, 0x67, 0xff, 0x24, 0x0c, 0x0d, 0x0e, 0x0f}, 20, 0}, - {0xC9, (uint8_t[]){0x33, 0x44, 0x44, 0x01}, 4, 0}, - {0xCF, (uint8_t[]){0x2C, 0x1E, 0x88, 0x58, 0x13, 0x18, 0x56, 0x18, 0x1E, 0x68, 0x88, 0x00, 0x65, 0x09, 0x22, 0xC4, 0x0C, 0x77, 0x22, 0x44, 0xAA, 0x55, 0x08, 0x08, 0x12, 0xA0, 0x08}, 27, 0}, - {0xD5, (uint8_t[]){0x40, 0x8E, 0x8D, 0x01, 0x35, 0x04, 0x92, 0x74, 0x04, 0x92, 0x74, 0x04, 0x08, 0x6A, 0x04, 0x46, 0x03, 0x03, 0x03, 0x03, 0x82, 0x01, 0x03, 0x00, 0xE0, 0x51, 0xA1, 0x00, 0x00, 0x00}, 30, 0}, - {0xD6, (uint8_t[]){0x10, 0x32, 0x54, 0x76, 0x98, 0xBA, 0xDC, 0xFE, 0x93, 0x00, 0x01, 0x83, 0x07, 0x07, 0x00, 0x07, 0x07, 0x00, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x00, 0x84, 0x00, 0x20, 0x01, 0x00}, 30, 0}, - {0xD7, (uint8_t[]){0x03, 0x01, 0x0b, 0x09, 0x0f, 0x0d, 0x1E, 0x1F, 0x18, 0x1d, 0x1f, 0x19, 0x40, 0x8E, 0x04, 0x00, 0x20, 0xA0, 0x1F}, 19, 0}, - {0xD8, (uint8_t[]){0x02, 0x00, 0x0a, 0x08, 0x0e, 0x0c, 0x1E, 0x1F, 0x18, 0x1d, 0x1f, 0x19}, 12, 0}, - {0xD9, (uint8_t[]){0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F}, 12, 0}, - {0xDD, (uint8_t[]){0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F}, 12, 0}, - {0xDF, (uint8_t[]){0x44, 0x73, 0x4B, 0x69, 0x00, 0x0A, 0x02, 0x90}, 8, 0}, - {0xE0, (uint8_t[]){0x3B, 0x28, 0x10, 0x16, 0x0c, 0x06, 0x11, 0x28, 0x5c, 0x21, 0x0D, 0x35, 0x13, 0x2C, 0x33, 0x28, 0x0D}, 17, 0}, - {0xE1, (uint8_t[]){0x37, 0x28, 0x10, 0x16, 0x0b, 0x06, 0x11, 0x28, 0x5C, 0x21, 0x0D, 0x35, 0x14, 0x2C, 0x33, 0x28, 0x0F}, 17, 0}, - {0xE2, (uint8_t[]){0x3B, 0x07, 0x12, 0x18, 0x0E, 0x0D, 0x17, 0x35, 0x44, 0x32, 0x0C, 0x14, 0x14, 0x36, 0x3A, 0x2F, 0x0D}, 17, 0}, - {0xE3, (uint8_t[]){0x37, 0x07, 0x12, 0x18, 0x0E, 0x0D, 0x17, 0x35, 0x44, 0x32, 0x0C, 0x14, 0x14, 0x36, 0x32, 0x2F, 0x0F}, 17, 0}, - {0xE4, (uint8_t[]){0x3B, 0x07, 0x12, 0x18, 0x0E, 0x0D, 0x17, 0x39, 0x44, 0x2E, 0x0C, 0x14, 0x14, 0x36, 0x3A, 0x2F, 0x0D}, 17, 0}, - {0xE5, (uint8_t[]){0x37, 0x07, 0x12, 0x18, 0x0E, 0x0D, 0x17, 0x39, 0x44, 0x2E, 0x0C, 0x14, 0x14, 0x36, 0x3A, 0x2F, 0x0F}, 17, 0}, - {0xA4, (uint8_t[]){0x85, 0x85, 0x95, 0x82, 0xAF, 0xAA, 0xAA, 0x80, 0x10, 0x30, 0x40, 0x40, 0x20, 0xFF, 0x60, 0x30}, 16, 0}, - {0xA4, (uint8_t[]){0x85, 0x85, 0x95, 0x85}, 4, 0}, - {0xBB, (uint8_t[]){0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, 8, 0}, - {0x13, (uint8_t[]){0x00}, 0, 0}, - {0x35, (uint8_t[]){0x00}, 1, 0}, // Enable Tearing Effect output (V-blank sync) - {0x11, (uint8_t[]){0x00}, 0, 120}, - {0x2C, (uint8_t[]){0x00, 0x00, 0x00, 0x00}, 4, 0} -}; - -void Axs15231bDisplay::lvgl_port_flush_callback(lv_display_t *drv, const lv_area_t *area, uint8_t *color_map) { - Axs15231bDisplay* self = (Axs15231bDisplay*)lv_display_get_user_data(drv); - assert(self != nullptr); - - assert(drv != nullptr); - esp_lcd_panel_handle_t panel_handle = (esp_lcd_panel_handle_t)lv_display_get_driver_data(drv); - assert(panel_handle != nullptr); - - // Swap RGB565 byte order for SPI transmission (big-endian wire format) - lv_draw_sw_rgb565_swap(color_map, lv_area_get_size(area)); - - lv_display_rotation_t rotation = lv_display_get_rotation(drv); - int logical_width = lv_display_get_horizontal_resolution(drv); - int logical_height = lv_display_get_vertical_resolution(drv); - - uint16_t* draw_buf = (uint16_t*)color_map; - - if (rotation != LV_DISPLAY_ROTATION_0) { - // Lazy-allocate tempBuf on first rotated frame - static bool allocationErrorLogged = false; - if (self->tempBuf == nullptr) { - self->tempBuf = (uint16_t *)heap_caps_malloc( - self->configuration->horizontalResolution * self->configuration->verticalResolution * sizeof(uint16_t), - MALLOC_CAP_SPIRAM); - if (self->tempBuf == nullptr) { - if (!allocationErrorLogged) { - LOG_E(TAG, "Failed to allocate rotation buffer, drawing unrotated"); - allocationErrorLogged = true; - } - if (esp_lcd_panel_draw_bitmap(panel_handle, 0, 0, self->configuration->horizontalResolution, self->configuration->verticalResolution, draw_buf) != ESP_OK) { - lv_display_flush_ready(drv); - } - return; - } - } - - // Rotate to tempBuf using tile-based approach for cache efficiency. - // SPIRAM random access is very slow due to cache thrashing. By processing - // in small tiles (TILE x TILE pixels), the working set stays within the - // ESP32-S3's 32KB PSRAM cache, dramatically reducing cache misses. - constexpr int TILE = 32; - uint16_t* src = (uint16_t*)color_map; - uint16_t* dst = self->tempBuf; - const int hw = self->configuration->horizontalResolution; - - switch (rotation) { - case LV_DISPLAY_ROTATION_90: - for (int ty = 0; ty < logical_height; ty += TILE) { - for (int tx = 0; tx < logical_width; tx += TILE) { - int y_end = (ty + TILE < logical_height) ? ty + TILE : logical_height; - int x_end = (tx + TILE < logical_width) ? tx + TILE : logical_width; - for (int y = ty; y < y_end; y++) { - for (int x = tx; x < x_end; x++) { - dst[(logical_width - 1 - x) * hw + y] = src[y * logical_width + x]; - } - } - } - } - break; - case LV_DISPLAY_ROTATION_180: { - // 180° is a simple reverse - sequential reads and writes - const int total = logical_width * logical_height; - for (int i = 0; i < total; i++) { - dst[total - 1 - i] = src[i]; - } - break; - } - case LV_DISPLAY_ROTATION_270: - for (int ty = 0; ty < logical_height; ty += TILE) { - for (int tx = 0; tx < logical_width; tx += TILE) { - int y_end = (ty + TILE < logical_height) ? ty + TILE : logical_height; - int x_end = (tx + TILE < logical_width) ? tx + TILE : logical_width; - for (int y = ty; y < y_end; y++) { - for (int x = tx; x < x_end; x++) { - dst[x * hw + (logical_height - 1 - y)] = src[y * logical_width + x]; - } - } - } - } - break; - default: - break; - } - draw_buf = self->tempBuf; - } - - // Wait for TE (tearing effect) signal before starting SPI transfer. - // This synchronizes frame output with the display's V-blank to reduce tearing. - if (self->teSyncSemaphore != nullptr) { - xSemaphoreTake(self->teSyncSemaphore, 0); // Drain any pending signal - xSemaphoreTake(self->teSyncSemaphore, pdMS_TO_TICKS(20)); // Wait for next V-blank - } - - esp_err_t ret = esp_lcd_panel_draw_bitmap(panel_handle, 0, 0, self->configuration->horizontalResolution, self->configuration->verticalResolution, draw_buf); - if (ret != ESP_OK) { - // If SPI transfer failed, on_color_trans_done won't fire. - // Manually signal flush ready to prevent LVGL from hanging. - LOG_E(TAG, "draw_bitmap failed: %s", esp_err_to_name(ret)); - lv_display_flush_ready(drv); - } -} - -bool Axs15231bDisplay::onColorTransDone(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_io_event_data_t *edata, void *user_ctx) { - lv_display_t *disp = (lv_display_t *)user_ctx; - if (disp != nullptr) { - lv_display_flush_ready(disp); - } - return false; -} - -void IRAM_ATTR Axs15231bDisplay::teIsrHandler(void* arg) { - SemaphoreHandle_t sem = (SemaphoreHandle_t)arg; - BaseType_t needYield = pdFALSE; - xSemaphoreGiveFromISR(sem, &needYield); - if (needYield == pdTRUE) { - portYIELD_FROM_ISR(); - } -} - -bool Axs15231bDisplay::setupTeSync() { - if (configuration->tePin == GPIO_NUM_NC) { - LOG_I(TAG, "TE pin not configured, skipping TE sync"); - return true; - } - - teSyncSemaphore = xSemaphoreCreateBinary(); - if (teSyncSemaphore == nullptr) { - LOG_E(TAG, "Failed to create TE sync semaphore"); - return false; - } - - gpio_config_t io_conf = {}; - io_conf.intr_type = GPIO_INTR_POSEDGE; - io_conf.mode = GPIO_MODE_INPUT; - io_conf.pull_up_en = GPIO_PULLUP_DISABLE; - io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE; - io_conf.pin_bit_mask = (1ULL << configuration->tePin); - - if (gpio_config(&io_conf) != ESP_OK) { - LOG_E(TAG, "Failed to configure TE GPIO"); - vSemaphoreDelete(teSyncSemaphore); - teSyncSemaphore = nullptr; - return false; - } - - esp_err_t err = gpio_install_isr_service(ESP_INTR_FLAG_IRAM); - if (err == ESP_OK) { - isrServiceInstalledByUs = true; - } else if (err != ESP_ERR_INVALID_STATE) { - LOG_E(TAG, "Failed to install GPIO ISR service"); - vSemaphoreDelete(teSyncSemaphore); - teSyncSemaphore = nullptr; - return false; - } - - if (gpio_isr_handler_add(configuration->tePin, teIsrHandler, (void*)teSyncSemaphore) != ESP_OK) { - LOG_E(TAG, "Failed to add TE ISR handler"); - gpio_intr_disable(configuration->tePin); - if (isrServiceInstalledByUs) { - gpio_uninstall_isr_service(); - isrServiceInstalledByUs = false; - } - vSemaphoreDelete(teSyncSemaphore); - teSyncSemaphore = nullptr; - return false; - } - - teIsrInstalled = true; - LOG_I(TAG, "TE sync enabled on GPIO %d", (int)configuration->tePin); - return true; -} - -void Axs15231bDisplay::teardownTeSync() { - if (teIsrInstalled && configuration->tePin != GPIO_NUM_NC) { - gpio_isr_handler_remove(configuration->tePin); - gpio_intr_disable(configuration->tePin); - teIsrInstalled = false; - } - if (isrServiceInstalledByUs) { - gpio_uninstall_isr_service(); - isrServiceInstalledByUs = false; - } - if (teSyncSemaphore != nullptr) { - vSemaphoreDelete(teSyncSemaphore); - teSyncSemaphore = nullptr; - } -} - -bool Axs15231bDisplay::createIoHandle() { - const esp_lcd_panel_io_spi_config_t panel_io_config = { - .cs_gpio_num = configuration->csPin, - .dc_gpio_num = configuration->dcPin, - .spi_mode = 3, - .pclk_hz = configuration->pixelClockFrequency, - .trans_queue_depth = configuration->transactionQueueDepth, - .on_color_trans_done = nullptr, - .user_ctx = nullptr, - .lcd_cmd_bits = 32, - .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 = 1, - .sio_mode = 0, - .lsb_first = 0, - .cs_high_active = 0 - } - }; - - return esp_lcd_new_panel_io_spi(configuration->spiHostDevice, &panel_io_config, &ioHandle) == ESP_OK; -} - -bool Axs15231bDisplay::createPanelHandle() { - const axs15231b_vendor_config_t vendor_config = { - .init_cmds = lcd_init_cmds, - .init_cmds_size = sizeof(lcd_init_cmds) / sizeof(lcd_init_cmds[0]), - .flags = { - .use_qspi_interface = 1, - }, - }; - - 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 = (void *)&vendor_config - }; - - if (esp_lcd_new_panel_axs15231b(ioHandle, &panel_config, &panelHandle) != ESP_OK) { - LOG_E(TAG, "Failed to create axs15231b"); - return false; - } - - if (esp_lcd_panel_reset(panelHandle) != ESP_OK) { - LOG_E(TAG, "Failed to reset panel"); - esp_lcd_panel_del(panelHandle); - panelHandle = nullptr; - return false; - } - - if (esp_lcd_panel_init(panelHandle) != ESP_OK) { - LOG_E(TAG, "Failed to init panel"); - esp_lcd_panel_del(panelHandle); - panelHandle = nullptr; - return false; - } - - //SWAPXY Doesn't work with the JC3248W535... Left in for future compatibility. - if (esp_lcd_panel_swap_xy(panelHandle, configuration->swapXY) != ESP_OK) { - LOG_E(TAG, "Failed to swap XY"); - esp_lcd_panel_del(panelHandle); - panelHandle = nullptr; - return false; - } - - if (esp_lcd_panel_mirror(panelHandle, configuration->mirrorX, configuration->mirrorY) != ESP_OK) { - LOG_E(TAG, "Failed to mirror panel"); - esp_lcd_panel_del(panelHandle); - panelHandle = nullptr; - return false; - } - - if (esp_lcd_panel_invert_color(panelHandle, configuration->invertColor) != ESP_OK) { - LOG_E(TAG, "Failed to invert color"); - esp_lcd_panel_del(panelHandle); - panelHandle = nullptr; - return false; - } - - if (esp_lcd_panel_disp_on_off(panelHandle, false) != ESP_OK) { - LOG_E(TAG, "Failed to turn off panel"); - esp_lcd_panel_del(panelHandle); - panelHandle = nullptr; - return false; - } - - return true; -} - -Axs15231bDisplay::Axs15231bDisplay(std::unique_ptr inConfiguration) : - configuration(std::move(inConfiguration)) -{ - assert(configuration != nullptr); -} - -bool Axs15231bDisplay::start() { - if (!createIoHandle()) { - LOG_E(TAG, "Failed to create IO handle"); - return false; - } - - if (!createPanelHandle()) { - LOG_E(TAG, "Failed to create panel handle"); - esp_lcd_panel_io_del(ioHandle); - ioHandle = nullptr; - return false; - } - - if (!setupTeSync()) { - LOG_W(TAG, "TE sync setup failed, continuing without TE synchronization"); - } - - return true; -} - -bool Axs15231bDisplay::stop() { - if (lvglDisplay != nullptr) { - stopLvgl(); - } - - teardownTeSync(); - - // Invalidate cached DisplayDriver - it holds a raw panelHandle that is about to be deleted - displayDriver.reset(); - - if (panelHandle != nullptr && esp_lcd_panel_del(panelHandle) != ESP_OK) { - LOG_E(TAG, "Failed to delete panel"); - } - panelHandle = nullptr; - - if (ioHandle != nullptr && esp_lcd_panel_io_del(ioHandle) != ESP_OK) { - LOG_E(TAG, "Failed to delete IO"); - } - ioHandle = nullptr; - - return true; -} - -bool Axs15231bDisplay::startLvgl() { - if (lvglDisplay != nullptr) { - LOG_E(TAG, "LVGL already started"); - return false; - } - - lvglDisplay = lv_display_create(configuration->horizontalResolution, configuration->verticalResolution); - lv_display_set_user_data(lvglDisplay, this); - lv_display_set_color_format(lvglDisplay, LV_COLOR_FORMAT_RGB565); - - uint32_t buffer_pixels = configuration->bufferSize; - uint32_t bufferSize = buffer_pixels * sizeof(uint16_t); - buffer1 = (uint16_t*)heap_caps_malloc(bufferSize, MALLOC_CAP_SPIRAM); - buffer2 = (uint16_t*)heap_caps_malloc(bufferSize, MALLOC_CAP_SPIRAM); - if (buffer1 == nullptr || buffer2 == nullptr) { - LOG_E(TAG, "Failed to allocate buffers"); - heap_caps_free(buffer1); - heap_caps_free(buffer2); - buffer1 = nullptr; - buffer2 = nullptr; - lv_display_delete(lvglDisplay); - lvglDisplay = nullptr; - return false; - } - - lv_display_set_buffers(lvglDisplay, buffer1, buffer2, bufferSize, LV_DISPLAY_RENDER_MODE_FULL); - - lv_display_set_flush_cb(lvglDisplay, lvgl_port_flush_callback); - - lv_display_set_driver_data(lvglDisplay, panelHandle); - - const esp_lcd_panel_io_callbacks_t cbs = { - .on_color_trans_done = onColorTransDone, - }; - if (esp_lcd_panel_io_register_event_callbacks(ioHandle, &cbs, lvglDisplay) != ESP_OK) { - LOG_E(TAG, "Failed to register panel IO callbacks"); - heap_caps_free(buffer1); - heap_caps_free(buffer2); - buffer1 = nullptr; - buffer2 = nullptr; - lv_display_delete(lvglDisplay); - lvglDisplay = nullptr; - return false; - } - - auto touch_device = getTouchDevice(); - if (touch_device != nullptr && touch_device->supportsLvgl()) { - touch_device->startLvgl(lvglDisplay); - } - - return true; -} - -bool Axs15231bDisplay::stopLvgl() { - if (lvglDisplay == nullptr) { - return false; - } - - auto touch_device = getTouchDevice(); - if (touch_device != nullptr) { - touch_device->stopLvgl(); - } - - esp_lcd_panel_disp_on_off(panelHandle, false); - - // Unregister IO callbacks before deleting the display to prevent use-after-free. - // The on_color_trans_done callback captures lvglDisplay as user_ctx; if a DMA - // transfer completes after lv_display_delete(), it would call - // lv_display_flush_ready() on a freed pointer. - const esp_lcd_panel_io_callbacks_t nullCbs = { - .on_color_trans_done = nullptr, - }; - esp_lcd_panel_io_register_event_callbacks(ioHandle, &nullCbs, nullptr); - - lv_display_delete(lvglDisplay); - lvglDisplay = nullptr; - - heap_caps_free(buffer1); - heap_caps_free(buffer2); - buffer1 = nullptr; - buffer2 = nullptr; - - heap_caps_free(tempBuf); - tempBuf = nullptr; - - return true; -} - -std::shared_ptr Axs15231bDisplay::getDisplayDriver() { - if (lvglDisplay != nullptr) { - LOG_E(TAG, "Cannot get DisplayDriver while LVGL is active - call stopLvgl() first"); - return nullptr; - } - if (panelHandle == nullptr) { - LOG_E(TAG, "Cannot get DisplayDriver - display is not started"); - return nullptr; - } - if (displayDriver == nullptr) { - displayDriver = std::make_shared( - panelHandle, - configuration->horizontalResolution, - configuration->verticalResolution, - tt::hal::display::ColorFormat::RGB565Swapped - ); - } - return displayDriver; -} diff --git a/Devices/guition-jc3248w535c/Source/Axs15231b/Axs15231bDisplay.h b/Devices/guition-jc3248w535c/Source/Axs15231b/Axs15231bDisplay.h deleted file mode 100644 index 18a3040d9..000000000 --- a/Devices/guition-jc3248w535c/Source/Axs15231b/Axs15231bDisplay.h +++ /dev/null @@ -1,139 +0,0 @@ -#pragma once - -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -class Axs15231bDisplay final : public tt::hal::display::DisplayDevice { - -public: - - class Configuration { - - public: - - Configuration( - spi_host_device_t spiHostDevice, - gpio_num_t csPin, - gpio_num_t dcPin, - gpio_num_t resetPin, - gpio_num_t tePin, - 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), - tePin(tePin), - 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; - gpio_num_t tePin; - 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: - - esp_lcd_panel_io_handle_t ioHandle = nullptr; - esp_lcd_panel_handle_t panelHandle = nullptr; - lv_display_t* lvglDisplay = nullptr; - uint16_t* buffer1 = nullptr; - uint16_t* buffer2 = nullptr; - uint16_t* tempBuf = nullptr; - SemaphoreHandle_t teSyncSemaphore = nullptr; - bool teIsrInstalled = false; - bool isrServiceInstalledByUs = false; - - std::unique_ptr configuration; - std::shared_ptr displayDriver; - - bool createIoHandle(); - - bool createPanelHandle(); - - bool setupTeSync(); - - void teardownTeSync(); - - static void IRAM_ATTR teIsrHandler(void* arg); - -public: - - explicit Axs15231bDisplay(std::unique_ptr inConfiguration); - - std::string getName() const override { return "AXS15231B Display"; } - - std::string getDescription() const override { return "AXS15231B display"; } - - bool start() override; - - bool stop() override; - - bool supportsLvgl() const override { return true; } - - bool startLvgl() override; - - bool stopLvgl() override; - - lv_display_t* getLvglDisplay() const override { return lvglDisplay; } - - 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; } - - bool supportsDisplayDriver() const override { return true; } - - std::shared_ptr getDisplayDriver() override; - - static void lvgl_port_flush_callback(lv_display_t *drv, const lv_area_t *area, uint8_t *color_map); - - static bool onColorTransDone(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_io_event_data_t *edata, void *user_ctx); -}; diff --git a/Devices/guition-jc3248w535c/Source/Axs15231b/Axs15231bTouch.cpp b/Devices/guition-jc3248w535c/Source/Axs15231b/Axs15231bTouch.cpp deleted file mode 100644 index b6bbb087b..000000000 --- a/Devices/guition-jc3248w535c/Source/Axs15231b/Axs15231bTouch.cpp +++ /dev/null @@ -1,38 +0,0 @@ -#include "Axs15231bTouch.h" - -#include -#include - -bool Axs15231bTouch::createIoHandle(esp_lcd_panel_io_handle_t& outHandle) { - if (configuration == nullptr) { - return false; - } - esp_lcd_panel_io_i2c_config_t io_config = ESP_LCD_TOUCH_IO_I2C_AXS15231B_CONFIG(); - return esp_lcd_new_panel_io_i2c(configuration->port, &io_config, &outHandle) == ESP_OK; -} - -bool Axs15231bTouch::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_axs15231b(ioHandle, &configuration, &panelHandle) == ESP_OK; -} - -esp_lcd_touch_config_t Axs15231bTouch::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/guition-jc3248w535c/Source/Axs15231b/Axs15231bTouch.h b/Devices/guition-jc3248w535c/Source/Axs15231b/Axs15231bTouch.h deleted file mode 100644 index 41013d21e..000000000 --- a/Devices/guition-jc3248w535c/Source/Axs15231b/Axs15231bTouch.h +++ /dev/null @@ -1,72 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include - -#include - -class Axs15231bTouch 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 Axs15231bTouch(std::unique_ptr inConfiguration) : configuration(std::move(inConfiguration)) { - assert(configuration != nullptr); - } - - std::string getName() const override { return "AXS15231B Touch"; } - - std::string getDescription() const override { return "AXS15231B I2C touch driver"; } -}; diff --git a/Devices/guition-jc3248w535c/Source/Configuration.cpp b/Devices/guition-jc3248w535c/Source/Configuration.cpp deleted file mode 100644 index cf1a8f69c..000000000 --- a/Devices/guition-jc3248w535c/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_1); -} - -extern const Configuration hardwareConfiguration = { - .initBoot = initBoot, - .createDevices = createDevices -}; diff --git a/Devices/guition-jc3248w535c/Source/devices/Display.cpp b/Devices/guition-jc3248w535c/Source/devices/Display.cpp deleted file mode 100644 index d35c9e561..000000000 --- a/Devices/guition-jc3248w535c/Source/devices/Display.cpp +++ /dev/null @@ -1,44 +0,0 @@ -#include "Display.h" - -#include -#include -#include - -static constexpr size_t JC3248W535C_LCD_DRAW_BUFFER_SIZE = 320 * 480; - -static std::shared_ptr createTouch() { - auto configuration = std::make_unique( - I2C_NUM_0, - 320, // width - 480, // height - false, // mirror_x - false, // mirror_y - false // swap_xy - ); - - return std::make_shared(std::move(configuration)); -} - -std::shared_ptr createDisplay() { - auto touch = createTouch(); - - auto configuration = std::make_unique( - SPI2_HOST, - GPIO_NUM_45, //CS - GPIO_NUM_NC, //DC - GPIO_NUM_NC, //RST - GPIO_NUM_38, //TE - 320, // width - 480, // height - touch, - false, // swap_xy - false, // mirror_x - false, // mirror_y - false, // invert color - JC3248W535C_LCD_DRAW_BUFFER_SIZE - ); - - configuration->backlightDutyFunction = driver::pwmbacklight::setBacklightDuty; - - return std::make_shared(std::move(configuration)); -} diff --git a/Devices/guition-jc3248w535c/Source/devices/Display.h b/Devices/guition-jc3248w535c/Source/devices/Display.h deleted file mode 100644 index 13a1fe144..000000000 --- a/Devices/guition-jc3248w535c/Source/devices/Display.h +++ /dev/null @@ -1,6 +0,0 @@ -#pragma once - -#include -#include - -std::shared_ptr createDisplay(); diff --git a/Devices/guition-jc3248w535c/device.properties b/Devices/guition-jc3248w535c/device.properties index 15da6f6eb..ff2553224 100644 --- a/Devices/guition-jc3248w535c/device.properties +++ b/Devices/guition-jc3248w535c/device.properties @@ -12,6 +12,8 @@ hardware.tinyUsb=true hardware.esptoolFlashFreq=120M hardware.bluetooth=true +dependencies.useDeprecatedHal=false + storage.userDataLocation=SD display.size=3.5" diff --git a/Devices/guition-jc3248w535c/devicetree.yaml b/Devices/guition-jc3248w535c/devicetree.yaml index a674aac66..6c1745429 100644 --- a/Devices/guition-jc3248w535c/devicetree.yaml +++ b/Devices/guition-jc3248w535c/devicetree.yaml @@ -2,4 +2,5 @@ dependencies: - Platforms/platform-esp32 - Drivers/dummy-i2s-amp-module - Drivers/audio-stream-module + - Drivers/axs15231b-module dts: guition,jc3248w535c.dts diff --git a/Devices/guition-jc3248w535c/guition,jc3248w535c.dts b/Devices/guition-jc3248w535c/guition,jc3248w535c.dts index 42d79b28c..39cda474b 100644 --- a/Devices/guition-jc3248w535c/guition,jc3248w535c.dts +++ b/Devices/guition-jc3248w535c/guition,jc3248w535c.dts @@ -9,8 +9,11 @@ #include #include #include -#include +#include +#include #include +#include +#include / { compatible = "root"; @@ -31,14 +34,6 @@ gpio-count = <49>; }; - i2c_internal: i2c0 { - compatible = "espressif,esp32-i2c"; - port = ; - clock-frequency = <400000>; - pin-sda = <&gpio0 4 GPIO_FLAG_NONE>; - pin-scl = <&gpio0 8 GPIO_FLAG_NONE>; - }; - i2c_external: i2c1 { compatible = "espressif,esp32-i2c"; port = ; @@ -60,6 +55,25 @@ i2s = <&i2s0>; }; + display_backlight_pwm { + compatible = "espressif,esp32-pwm-ledc"; + pin = <&gpio0 1 GPIO_FLAG_NONE>; + period-ns = <25000>; + ledc-timer = <0>; + ledc-channel = <0>; + // Matches the deprecated HAL's old PwmBacklight driver, which hardcoded 8-bit duty + // resolution (the binding's own default is 10-bit). + duty-resolution = <8>; + }; + + 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 = ; @@ -70,8 +84,73 @@ pin-wp = <&gpio0 40 GPIO_FLAG_NONE>; pin-hd = <&gpio0 39 GPIO_FLAG_NONE>; - display { - compatible = "display-placeholder"; + display@0 { + compatible = "axs,axs15231b"; + horizontal-resolution = <320>; + vertical-resolution = <480>; + pin-te = <&gpio0 38 GPIO_FLAG_NONE>; + backlight = <&display_backlight>; + + // Vendor bring-up sequence, carried over unchanged from the deprecated HAL's old + // Axs15231bDisplay.cpp. Framing is [cmd, data-length, delay-ms, data-length bytes of + // data...] - see axs15231b-module's init-sequence binding property for the encoding. + init-sequence = [ + 0xBB 8 0 0x00 0x00 0x00 0x00 0x00 0x00 0x5A 0xA5 + 0xA0 17 0 0xC0 0x10 0x00 0x02 0x00 0x00 0x04 0x3F 0x20 0x05 0x3F 0x3F 0x00 0x00 0x00 0x00 0x00 + 0xA2 31 0 0x30 0x3C 0x24 0x14 0xD0 0x20 0xFF 0xE0 0x40 0x19 0x80 0x80 0x80 0x20 0xf9 0x10 0x02 0xff 0xff 0xF0 0x90 0x01 0x32 0xA0 0x91 0xE0 0x20 0x7F 0xFF 0x00 0x5A + 0xD0 30 0 0xE0 0x40 0x51 0x24 0x08 0x05 0x10 0x01 0x20 0x15 0x42 0xC2 0x22 0x22 0xAA 0x03 0x10 0x12 0x60 0x14 0x1E 0x51 0x15 0x00 0x8A 0x20 0x00 0x03 0x3A 0x12 + 0xA3 22 0 0xA0 0x06 0xAa 0x00 0x08 0x02 0x0A 0x04 0x04 0x04 0x04 0x04 0x04 0x04 0x04 0x04 0x04 0x04 0x04 0x00 0x55 0x55 + 0xC1 30 0 0x31 0x04 0x02 0x02 0x71 0x05 0x24 0x55 0x02 0x00 0x41 0x00 0x53 0xFF 0xFF 0xFF 0x4F 0x52 0x00 0x4F 0x52 0x00 0x45 0x3B 0x0B 0x02 0x0d 0x00 0xFF 0x40 + 0xC3 11 0 0x00 0x00 0x00 0x50 0x03 0x00 0x00 0x00 0x01 0x80 0x01 + 0xC4 29 0 0x00 0x24 0x33 0x80 0x00 0xea 0x64 0x32 0xC8 0x64 0xC8 0x32 0x90 0x90 0x11 0x06 0xDC 0xFA 0x00 0x00 0x80 0xFE 0x10 0x10 0x00 0x0A 0x0A 0x44 0x50 + 0xC5 23 0 0x18 0x00 0x00 0x03 0xFE 0x3A 0x4A 0x20 0x30 0x10 0x88 0xDE 0x0D 0x08 0x0F 0x0F 0x01 0x3A 0x4A 0x20 0x10 0x10 0x00 + 0xC6 20 0 0x05 0x0A 0x05 0x0A 0x00 0xE0 0x2E 0x0B 0x12 0x22 0x12 0x22 0x01 0x03 0x00 0x3F 0x6A 0x18 0xC8 0x22 + 0xC7 20 0 0x50 0x32 0x28 0x00 0xa2 0x80 0x8f 0x00 0x80 0xff 0x07 0x11 0x9c 0x67 0xff 0x24 0x0c 0x0d 0x0e 0x0f + 0xC9 4 0 0x33 0x44 0x44 0x01 + 0xCF 27 0 0x2C 0x1E 0x88 0x58 0x13 0x18 0x56 0x18 0x1E 0x68 0x88 0x00 0x65 0x09 0x22 0xC4 0x0C 0x77 0x22 0x44 0xAA 0x55 0x08 0x08 0x12 0xA0 0x08 + 0xD5 30 0 0x40 0x8E 0x8D 0x01 0x35 0x04 0x92 0x74 0x04 0x92 0x74 0x04 0x08 0x6A 0x04 0x46 0x03 0x03 0x03 0x03 0x82 0x01 0x03 0x00 0xE0 0x51 0xA1 0x00 0x00 0x00 + 0xD6 30 0 0x10 0x32 0x54 0x76 0x98 0xBA 0xDC 0xFE 0x93 0x00 0x01 0x83 0x07 0x07 0x00 0x07 0x07 0x00 0x03 0x03 0x03 0x03 0x03 0x03 0x00 0x84 0x00 0x20 0x01 0x00 + 0xD7 19 0 0x03 0x01 0x0b 0x09 0x0f 0x0d 0x1E 0x1F 0x18 0x1d 0x1f 0x19 0x40 0x8E 0x04 0x00 0x20 0xA0 0x1F + 0xD8 12 0 0x02 0x00 0x0a 0x08 0x0e 0x0c 0x1E 0x1F 0x18 0x1d 0x1f 0x19 + 0xD9 12 0 0x1F 0x1F 0x1F 0x1F 0x1F 0x1F 0x1F 0x1F 0x1F 0x1F 0x1F 0x1F + 0xDD 12 0 0x1F 0x1F 0x1F 0x1F 0x1F 0x1F 0x1F 0x1F 0x1F 0x1F 0x1F 0x1F + 0xDF 8 0 0x44 0x73 0x4B 0x69 0x00 0x0A 0x02 0x90 + 0xE0 17 0 0x3B 0x28 0x10 0x16 0x0c 0x06 0x11 0x28 0x5c 0x21 0x0D 0x35 0x13 0x2C 0x33 0x28 0x0D + 0xE1 17 0 0x37 0x28 0x10 0x16 0x0b 0x06 0x11 0x28 0x5C 0x21 0x0D 0x35 0x14 0x2C 0x33 0x28 0x0F + 0xE2 17 0 0x3B 0x07 0x12 0x18 0x0E 0x0D 0x17 0x35 0x44 0x32 0x0C 0x14 0x14 0x36 0x3A 0x2F 0x0D + 0xE3 17 0 0x37 0x07 0x12 0x18 0x0E 0x0D 0x17 0x35 0x44 0x32 0x0C 0x14 0x14 0x36 0x32 0x2F 0x0F + 0xE4 17 0 0x3B 0x07 0x12 0x18 0x0E 0x0D 0x17 0x39 0x44 0x2E 0x0C 0x14 0x14 0x36 0x3A 0x2F 0x0D + 0xE5 17 0 0x37 0x07 0x12 0x18 0x0E 0x0D 0x17 0x39 0x44 0x2E 0x0C 0x14 0x14 0x36 0x3A 0x2F 0x0F + 0xA4 16 0 0x85 0x85 0x95 0x82 0xAF 0xAA 0xAA 0x80 0x10 0x30 0x40 0x40 0x20 0xFF 0x60 0x30 + 0xA4 4 0 0x85 0x85 0x95 0x85 + 0xBB 8 0 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 + 0x13 0 0 + 0x35 1 0 0x00 + 0x11 0 120 + 0x2C 4 0 0x00 0x00 0x00 0x00 + ]; + }; + }; + + // Declared (and thus started) after display@0 above, not before: the display and touch + // sides of this chip share the same silicon, and the deprecated HAL's old Hal.cpp always + // started the display first, then the touch device second. Starting touch first (as an + // earlier version of this devicetree did) let touch's I2C handshake succeed initially, but + // the display's own reset/init sequence running afterward then reset the whole chip and + // left the touch interface unresponsive - confirmed on real hardware (I2C writes started + // failing consistently right after display bring-up completed). + i2c_internal: i2c0 { + compatible = "espressif,esp32-i2c"; + port = ; + clock-frequency = <400000>; + pin-sda = <&gpio0 4 GPIO_FLAG_NONE>; + pin-scl = <&gpio0 8 GPIO_FLAG_NONE>; + + touch { + compatible = "axs,axs15231b-touch"; + reg = <0x3B>; + x-max = <320>; + y-max = <480>; }; }; diff --git a/Devices/guition-jc3248w535c/Source/module.cpp b/Devices/guition-jc3248w535c/source/module.cpp similarity index 88% rename from Devices/guition-jc3248w535c/Source/module.cpp rename to Devices/guition-jc3248w535c/source/module.cpp index a8ef4696a..b649c4192 100644 --- a/Devices/guition-jc3248w535c/Source/module.cpp +++ b/Devices/guition-jc3248w535c/source/module.cpp @@ -3,12 +3,10 @@ extern "C" { static error_t start() { - // Empty for now return ERROR_NONE; } static error_t stop() { - // Empty for now return ERROR_NONE; } diff --git a/Devices/guition-jc8048w550c/CMakeLists.txt b/Devices/guition-jc8048w550c/CMakeLists.txt index c6e98b904..a832a83a2 100644 --- a/Devices/guition-jc8048w550c/CMakeLists.txt +++ b/Devices/guition-jc8048w550c/CMakeLists.txt @@ -1,7 +1,6 @@ -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 RgbDisplay GT911 PwmBacklight driver vfs fatfs + REQUIRES TactilityKernel driver ) diff --git a/Devices/guition-jc8048w550c/Source/Configuration.cpp b/Devices/guition-jc8048w550c/Source/Configuration.cpp deleted file mode 100644 index 3e384edb3..000000000 --- a/Devices/guition-jc8048w550c/Source/Configuration.cpp +++ /dev/null @@ -1,21 +0,0 @@ -#include "PwmBacklight.h" -#include "devices/Display.h" - -#include - -using namespace tt::hal; - -static bool initBoot() { - return driver::pwmbacklight::init(GPIO_NUM_2); -} - -static DeviceVector createDevices() { - return { - createDisplay(), - }; -} - -extern const Configuration hardwareConfiguration = { - .initBoot = initBoot, - .createDevices = createDevices -}; diff --git a/Devices/guition-jc8048w550c/Source/devices/Display.cpp b/Devices/guition-jc8048w550c/Source/devices/Display.cpp deleted file mode 100644 index 06ddbc349..000000000 --- a/Devices/guition-jc8048w550c/Source/devices/Display.cpp +++ /dev/null @@ -1,108 +0,0 @@ -#include "Display.h" - -#include -#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 = 16000000, - .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_39, - .vsync_gpio_num = GPIO_NUM_41, - .de_gpio_num = GPIO_NUM_40 , - .pclk_gpio_num = GPIO_NUM_42, - .disp_gpio_num = GPIO_NUM_NC, - .data_gpio_nums = { - GPIO_NUM_8, // B3 - GPIO_NUM_3, // B4 - GPIO_NUM_46, // B5 - GPIO_NUM_9, // B6 - GPIO_NUM_1, // B7 - GPIO_NUM_5, // G2 - GPIO_NUM_6, // G3 - GPIO_NUM_7, // G4 - GPIO_NUM_15, // G5 - GPIO_NUM_16, // G6 - GPIO_NUM_4, // G7 - GPIO_NUM_45, // R3 - GPIO_NUM_48, // R4 - GPIO_NUM_47, // R5 - GPIO_NUM_21, // R6 - GPIO_NUM_14, // 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, - driver::pwmbacklight::setBacklightDuty - ); - - return std::make_shared(std::move(configuration)); -} diff --git a/Devices/guition-jc8048w550c/Source/devices/Display.h b/Devices/guition-jc8048w550c/Source/devices/Display.h deleted file mode 100644 index 7a9b967d1..000000000 --- a/Devices/guition-jc8048w550c/Source/devices/Display.h +++ /dev/null @@ -1,5 +0,0 @@ -#pragma once - -#include - -std::shared_ptr createDisplay(); diff --git a/Devices/guition-jc8048w550c/device.properties b/Devices/guition-jc8048w550c/device.properties index 3cebf4d42..4ad495a8a 100644 --- a/Devices/guition-jc8048w550c/device.properties +++ b/Devices/guition-jc8048w550c/device.properties @@ -11,6 +11,8 @@ hardware.spiRamSpeed=80M hardware.esptoolFlashFreq=80M hardware.bluetooth=true +dependencies.useDeprecatedHal=false + storage.userDataLocation=SD display.size=5" diff --git a/Devices/guition-jc8048w550c/devicetree.yaml b/Devices/guition-jc8048w550c/devicetree.yaml index a4e919e7c..02cb31d9b 100644 --- a/Devices/guition-jc8048w550c/devicetree.yaml +++ b/Devices/guition-jc8048w550c/devicetree.yaml @@ -2,4 +2,6 @@ dependencies: - Platforms/platform-esp32 - Drivers/dummy-i2s-amp-module - Drivers/audio-stream-module + - Drivers/rgb-display-module + - Drivers/gt911-module dts: guition,jc8048w550c.dts diff --git a/Devices/guition-jc8048w550c/guition,jc8048w550c.dts b/Devices/guition-jc8048w550c/guition,jc8048w550c.dts index 78757856b..3be39b67a 100644 --- a/Devices/guition-jc8048w550c/guition,jc8048w550c.dts +++ b/Devices/guition-jc8048w550c/guition,jc8048w550c.dts @@ -9,7 +9,11 @@ #include #include #include +#include +#include #include +#include +#include / { compatible = "root"; @@ -36,6 +40,16 @@ clock-frequency = <400000>; pin-sda = <&gpio0 19 GPIO_FLAG_NONE>; pin-scl = <&gpio0 20 GPIO_FLAG_NONE>; + + touch0 { + // Reset pin 38 and interrupt pin 18 (or INT = NC) exist on the board but are not + // wired up here - conflicting schematic info, unverified (matches the original + // deprecated-HAL config). + compatible = "goodix,gt911"; + reg = <0x5D>; + x-max = <800>; + y-max = <480>; + }; }; i2c_external: i2c1 { @@ -81,4 +95,58 @@ pin-tx = <&gpio0 18 GPIO_FLAG_NONE>; pin-rx = <&gpio0 17 GPIO_FLAG_NONE>; }; + + display_backlight_pwm { + compatible = "espressif,esp32-pwm-ledc"; + pin = <&gpio0 2 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>; + }; + + 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 39 GPIO_FLAG_NONE>; + pin-vsync = <&gpio0 41 GPIO_FLAG_NONE>; + pin-de = <&gpio0 40 GPIO_FLAG_NONE>; + pin-pclk = <&gpio0 42 GPIO_FLAG_NONE>; + pin-data0 = <&gpio0 8 GPIO_FLAG_NONE>; // B3 + pin-data1 = <&gpio0 3 GPIO_FLAG_NONE>; // B4 + pin-data2 = <&gpio0 46 GPIO_FLAG_NONE>; // B5 + pin-data3 = <&gpio0 9 GPIO_FLAG_NONE>; // B6 + pin-data4 = <&gpio0 1 GPIO_FLAG_NONE>; // B7 + pin-data5 = <&gpio0 5 GPIO_FLAG_NONE>; // G2 + pin-data6 = <&gpio0 6 GPIO_FLAG_NONE>; // G3 + pin-data7 = <&gpio0 7 GPIO_FLAG_NONE>; // G4 + pin-data8 = <&gpio0 15 GPIO_FLAG_NONE>; // G5 + pin-data9 = <&gpio0 16 GPIO_FLAG_NONE>; // G6 + pin-data10 = <&gpio0 4 GPIO_FLAG_NONE>; // G7 + pin-data11 = <&gpio0 45 GPIO_FLAG_NONE>; // R3 + pin-data12 = <&gpio0 48 GPIO_FLAG_NONE>; // R4 + pin-data13 = <&gpio0 47 GPIO_FLAG_NONE>; // R5 + pin-data14 = <&gpio0 21 GPIO_FLAG_NONE>; // R6 + pin-data15 = <&gpio0 14 GPIO_FLAG_NONE>; // R7 + backlight = <&display_backlight>; + }; }; diff --git a/Devices/guition-jc8048w550c/Source/module.cpp b/Devices/guition-jc8048w550c/source/module.cpp similarity index 88% rename from Devices/guition-jc8048w550c/Source/module.cpp rename to Devices/guition-jc8048w550c/source/module.cpp index 2772cd6ca..542bf9fa1 100644 --- a/Devices/guition-jc8048w550c/Source/module.cpp +++ b/Devices/guition-jc8048w550c/source/module.cpp @@ -3,12 +3,10 @@ extern "C" { static error_t start() { - // Empty for now return ERROR_NONE; } static error_t stop() { - // Empty for now return ERROR_NONE; } diff --git a/Devices/heltec-wifi-lora-32-v3/CMakeLists.txt b/Devices/heltec-wifi-lora-32-v3/CMakeLists.txt index 1a17c5f37..a7d3e6712 100644 --- a/Devices/heltec-wifi-lora-32-v3/CMakeLists.txt +++ b/Devices/heltec-wifi-lora-32-v3/CMakeLists.txt @@ -1,7 +1,6 @@ -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 EspLcdCompat SSD1306 ButtonControl EstimatedPower driver + REQUIRES TactilityKernel ) diff --git a/Devices/heltec-wifi-lora-32-v3/Source/Configuration.cpp b/Devices/heltec-wifi-lora-32-v3/Source/Configuration.cpp deleted file mode 100644 index 70fb18d39..000000000 --- a/Devices/heltec-wifi-lora-32-v3/Source/Configuration.cpp +++ /dev/null @@ -1,48 +0,0 @@ -#include "devices/Display.h" -#include "devices/Power.h" -#include "devices/Constants.h" - -#include -#include - -#include -#include - -#include - -static void enableOledPower() { - gpio_config_t io_conf = { - .pin_bit_mask = (1ULL << DISPLAY_PIN_POWER), - .mode = GPIO_MODE_OUTPUT, - .pull_up_en = GPIO_PULLUP_DISABLE, // The board has an external pull-up - .pull_down_en = GPIO_PULLDOWN_DISABLE, - .intr_type = GPIO_INTR_DISABLE, - }; - gpio_config(&io_conf); - gpio_set_level(DISPLAY_PIN_POWER, 0); // Active low - - vTaskDelay(pdMS_TO_TICKS(500)); // Add a small delay for power to stabilize - LOG_I("HeltecV3", "OLED power enabled"); -} - -static bool initBoot() { - // Enable power to the OLED before doing anything else - enableOledPower(); - - return true; -} - -using namespace tt::hal; - -static std::vector> createDevices() { - return { - createPower(), - ButtonControl::createOneButtonControl(0), - createDisplay() - }; -} - -extern const Configuration hardwareConfiguration = { - .initBoot = initBoot, - .createDevices = createDevices -}; diff --git a/Devices/heltec-wifi-lora-32-v3/Source/devices/Constants.h b/Devices/heltec-wifi-lora-32-v3/Source/devices/Constants.h deleted file mode 100644 index 27217d14d..000000000 --- a/Devices/heltec-wifi-lora-32-v3/Source/devices/Constants.h +++ /dev/null @@ -1,16 +0,0 @@ -#pragma once - -#include -#include -#include - -// Display -constexpr auto DISPLAY_I2C_ADDRESS = 0x3C; -constexpr auto DISPLAY_I2C_SPEED = 200000; -constexpr auto DISPLAY_I2C_PORT = I2C_NUM_0; -constexpr auto DISPLAY_PIN_SDA = GPIO_NUM_17; -constexpr auto DISPLAY_PIN_SCL = GPIO_NUM_18; -constexpr auto DISPLAY_PIN_RST = GPIO_NUM_21; -constexpr auto DISPLAY_HORIZONTAL_RESOLUTION = 128; -constexpr auto DISPLAY_VERTICAL_RESOLUTION = 64; -constexpr auto DISPLAY_PIN_POWER = GPIO_NUM_36; diff --git a/Devices/heltec-wifi-lora-32-v3/Source/devices/Display.cpp b/Devices/heltec-wifi-lora-32-v3/Source/devices/Display.cpp deleted file mode 100644 index 0a18d1ed3..000000000 --- a/Devices/heltec-wifi-lora-32-v3/Source/devices/Display.cpp +++ /dev/null @@ -1,19 +0,0 @@ -#include "Display.h" -#include "Constants.h" - -#include - -std::shared_ptr createDisplay() { - auto configuration = std::make_unique( - DISPLAY_I2C_PORT, - DISPLAY_I2C_ADDRESS, - DISPLAY_PIN_RST, - DISPLAY_HORIZONTAL_RESOLUTION, - DISPLAY_VERTICAL_RESOLUTION, - nullptr, // no touch - false // invert - ); - - auto display = std::make_shared(std::move(configuration)); - return std::static_pointer_cast(display); -} \ No newline at end of file diff --git a/Devices/heltec-wifi-lora-32-v3/Source/devices/Display.h b/Devices/heltec-wifi-lora-32-v3/Source/devices/Display.h deleted file mode 100644 index 7a9b967d1..000000000 --- a/Devices/heltec-wifi-lora-32-v3/Source/devices/Display.h +++ /dev/null @@ -1,5 +0,0 @@ -#pragma once - -#include - -std::shared_ptr createDisplay(); diff --git a/Devices/heltec-wifi-lora-32-v3/Source/devices/Power.cpp b/Devices/heltec-wifi-lora-32-v3/Source/devices/Power.cpp deleted file mode 100644 index 652bbc209..000000000 --- a/Devices/heltec-wifi-lora-32-v3/Source/devices/Power.cpp +++ /dev/null @@ -1,20 +0,0 @@ -#include "Power.h" - -#include -#include -#include -#include - -// ADC enable pin on GPIO37 -constexpr auto ADC_CTRL = 37; - -std::shared_ptr createPower() { - ChargeFromAdcVoltage::Configuration configuration; - // 2.0 ratio, but +0.11 added as display voltage sag compensation. - configuration.adcMultiplier = 2.11; - - // Configure the ADC enable pin as an output - tt::hal::gpio::configure(ADC_CTRL, tt::hal::gpio::Mode::Output, false, false); - - return std::make_shared(configuration); -} \ No newline at end of file diff --git a/Devices/heltec-wifi-lora-32-v3/Source/devices/Power.h b/Devices/heltec-wifi-lora-32-v3/Source/devices/Power.h deleted file mode 100644 index a878df85f..000000000 --- a/Devices/heltec-wifi-lora-32-v3/Source/devices/Power.h +++ /dev/null @@ -1,7 +0,0 @@ -#pragma once - -#include -#include -#include - -std::shared_ptr createPower(); diff --git a/Devices/heltec-wifi-lora-32-v3/device.properties b/Devices/heltec-wifi-lora-32-v3/device.properties index 729c48738..2fe658dda 100644 --- a/Devices/heltec-wifi-lora-32-v3/device.properties +++ b/Devices/heltec-wifi-lora-32-v3/device.properties @@ -12,6 +12,8 @@ hardware.tinyUsb=true hardware.esptoolFlashFreq=120M hardware.bluetooth=true +dependencies.useDeprecatedHal=false + storage.userDataLocation=Internal display.size=0.96" diff --git a/Devices/heltec-wifi-lora-32-v3/devicetree.yaml b/Devices/heltec-wifi-lora-32-v3/devicetree.yaml index 41589b61b..9b552609c 100644 --- a/Devices/heltec-wifi-lora-32-v3/devicetree.yaml +++ b/Devices/heltec-wifi-lora-32-v3/devicetree.yaml @@ -1,3 +1,5 @@ dependencies: - Platforms/platform-esp32 + - Drivers/ssd1306-module + - Drivers/button-control-module dts: heltec,wifi-lora-32-v3.dts diff --git a/Devices/heltec-wifi-lora-32-v3/heltec,wifi-lora-32-v3.dts b/Devices/heltec-wifi-lora-32-v3/heltec,wifi-lora-32-v3.dts index 4c5448a8c..73c57b7b3 100644 --- a/Devices/heltec-wifi-lora-32-v3/heltec,wifi-lora-32-v3.dts +++ b/Devices/heltec-wifi-lora-32-v3/heltec,wifi-lora-32-v3.dts @@ -5,6 +5,11 @@ #include #include #include +#include +#include +#include +#include +#include / { compatible = "root"; @@ -25,11 +30,87 @@ gpio-count = <49>; }; + // Board power-enable pins. Must be asserted before the dependent devices below start, since + // devicetree devices are constructed and started during kernel_init(), earlier than the + // deprecated HAL's initBoot() used to run. gpio-hog nodes run in declaration order, so they + // must stay before their dependents. + oled_power_en { + // Active low (external pull-up on the board). + compatible = "gpio-hog"; + pin = <&gpio0 36 GPIO_FLAG_NONE>; + mode = ; + }; + + adc_ctrl_en { + compatible = "gpio-hog"; + pin = <&gpio0 37 GPIO_FLAG_NONE>; + mode = ; + }; + i2c_internal { compatible = "espressif,esp32-i2c"; port = ; clock-frequency = <200000>; pin-sda = <&gpio0 17 GPIO_FLAG_NONE>; pin-scl = <&gpio0 18 GPIO_FLAG_NONE>; + + display0 { + compatible = "solomon,ssd1306"; + reg = <0x3C>; + horizontal-resolution = <128>; + vertical-resolution = <64>; + pin-reset = <&gpio0 21 GPIO_FLAG_NONE>; + // oled_power_en above only just asserted the OLED's power rail moments earlier in + // the same kernel_init() pass - matches the deprecated HAL's old + // enableOledPower()'s explicit 500ms settle delay before touching the chip. + power-on-delay-ms = <500>; + + // Vendor bring-up sequence, carried over unchanged from the deprecated HAL's old + // Ssd1306Display.cpp (its own comment: "esp_lcd_panel_init() ... doesn't configure + // correctly for Heltec V3!"). Each byte is its own SSD1306 command - see + // ssd1306-module's init-sequence binding property for the encoding. + init-sequence = [ + 0xAE // Display off while configuring + 0xD5 0xF0 // Set oscillator frequency (~96 Hz) - must come early in the sequence + 0xA8 0x3F // Set multiplex ratio (vertical-resolution - 1 = 64 - 1) + 0xD3 0x00 // Set display offset + 0x40 // Set display start line = 0 + 0x8D 0x14 // Enable charge pump - must be before memory mode + 0x20 0x00 // Horizontal addressing mode + 0xA1 // Segment remap (Heltec V3 orientation) + 0xC8 // Scan direction reversed + 0xDA 0x12 // COM pin config (alternate, for 64-row displays) + 0x81 0xCF // Contrast + 0xD9 0xF1 // Pre-charge period + 0xDB 0x40 // VCOMH deselect level + 0x21 0x00 0x7F // Column address range: 0 to (horizontal-resolution - 1) + 0x22 0x00 0x07 // Page address range: 0 to 7 (for a 64-row display) + 0xA7 // Display invert - required for this panel's wiring + 0x2E // Stop scroll + 0xAF // Display on + ]; + }; + }; + + adc0 { + compatible = "espressif,esp32-adc-oneshot"; + unit-id = ; + channels = ; + }; + + // Matches the deprecated HAL's old Power.cpp: ADC1 CH3 behind a 2:1 divider, with +0.11 + // added as display voltage sag compensation (adcMultiplier = 2.11, so multiplier = 2110). + // reference-voltage-mv is the nominal full-scale value at ADC_ATTEN_DB_12 (battery-sense has + // no calibration path, unlike the old driver's raw-scaling formula). + battery-sense { + compatible = "battery-sense"; + io-channel = <&adc0 0>; + reference-voltage-mv = <3300>; + multiplier = <2110>; + }; + + buttons { + compatible = "tactility,button-control"; + pin-primary = <&gpio0 0 GPIO_FLAG_NONE>; }; }; diff --git a/Devices/heltec-wifi-lora-32-v3/Source/module.cpp b/Devices/heltec-wifi-lora-32-v3/source/module.cpp similarity index 88% rename from Devices/heltec-wifi-lora-32-v3/Source/module.cpp rename to Devices/heltec-wifi-lora-32-v3/source/module.cpp index 725a5efcb..fa497d6f3 100644 --- a/Devices/heltec-wifi-lora-32-v3/Source/module.cpp +++ b/Devices/heltec-wifi-lora-32-v3/source/module.cpp @@ -3,12 +3,10 @@ extern "C" { static error_t start() { - // Empty for now return ERROR_NONE; } static error_t stop() { - // Empty for now return ERROR_NONE; } diff --git a/Devices/lilygo-tdeck-max/CMakeLists.txt b/Devices/lilygo-tdeck-max/CMakeLists.txt index 53724a09e..6328ca3f6 100644 --- a/Devices/lilygo-tdeck-max/CMakeLists.txt +++ b/Devices/lilygo-tdeck-max/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 GDEQ031T10 TCA8418 BQ27220 driver + INCLUDE_DIRS "source" + REQUIRES Tactility GDEQ031T10 driver ) diff --git a/Devices/lilygo-tdeck-max/Source/Configuration.cpp b/Devices/lilygo-tdeck-max/Source/Configuration.cpp deleted file mode 100644 index 228ba8d6d..000000000 --- a/Devices/lilygo-tdeck-max/Source/Configuration.cpp +++ /dev/null @@ -1,100 +0,0 @@ -#include "devices/Display.h" -#include "devices/TdeckmaxKeyboard.h" -#include "devices/TdeckmaxPower.h" - -#include - -#include -#include -#include -#include -#include -#include - -using namespace tt::hal; - -// Reset lines routed through the XL9555 IO expander (P-numbers from the vendor -// lib/TDeckMaxBoard/src/TDeckMaxBoard.h). Both are active low. -constexpr auto XL9555_PIN_TOUCH_RST = 7; // P07 -constexpr auto XL9555_PIN_KEY_RST = 9; // P11 - -// Release the touch and keyboard reset lines held by the XL9555. Without this -// the touch controller may stay in a half-powered state and the keyboard's -// TCA8418 is held in reset, so neither responds. Mirrors the vendor factory -// firmware's XL9555 init (examples/factory/factory.ino). -static void initIoExpander() { - auto* xl9555 = device_find_by_name("xl9555"); - if (xl9555 == nullptr) { - // Boot anyway; display works without the expander, but touch/keyboard won't. - return; - } - - auto* touch_rst = gpio_descriptor_acquire(xl9555, XL9555_PIN_TOUCH_RST, GPIO_OWNER_GPIO); - auto* key_rst = gpio_descriptor_acquire(xl9555, XL9555_PIN_KEY_RST, GPIO_OWNER_GPIO); - - if (touch_rst != nullptr) { - gpio_descriptor_set_flags(touch_rst, GPIO_FLAG_DIRECTION_OUTPUT); - gpio_descriptor_set_level(touch_rst, false); - } - if (key_rst != nullptr) { - gpio_descriptor_set_flags(key_rst, GPIO_FLAG_DIRECTION_OUTPUT); - gpio_descriptor_set_level(key_rst, false); - } - - delay_millis(20); - - // Release both resets and give the controllers time to boot before they're probed. - if (touch_rst != nullptr) { - gpio_descriptor_set_level(touch_rst, true); - gpio_descriptor_release(touch_rst); - } - if (key_rst != nullptr) { - gpio_descriptor_set_level(key_rst, true); - gpio_descriptor_release(key_rst); - } - - delay_millis(60); -} - -static bool initBoot() { - // The LoRa and SD chip-selects share the EPD's SPI bus but aren't wired up - // yet. spi0's start() already drives every cs-gpio high during kernel init; - // re-assert deselection before the EPD driver first transacts, as a cheap - // guard against either chip latching stray EPD command bytes (same pattern - // as the esp32_sdspi mount path). - auto* spi0 = device_find_by_name("spi0"); - check(spi0 != nullptr); - esp32_spi_deselect_all_cs(spi0); - - initIoExpander(); - - return true; -} - -static DeviceVector createDevices() { - auto* i2c = device_find_by_name("i2c0"); - check(i2c != nullptr); - - // SD card is not created here: it's an espressif,esp32-sdspi node in the - // devicetree (sdcard@1 under spi0), started by Hal::init's mountAll(). - DeviceVector devices = { - createDisplay() - }; - - auto keypad = std::make_shared(i2c); - devices.push_back(keypad); - devices.push_back(std::make_shared(keypad)); - - // Battery metrics from the BQ27220 fuel gauge (0x55); power-off via the - // SY6970 charger (0x6A). Enables the launcher's power-off button. - auto gauge = std::make_shared(i2c); - devices.push_back(gauge); - devices.push_back(std::make_shared(gauge, i2c)); - - return devices; -} - -extern const Configuration hardwareConfiguration = { - .initBoot = initBoot, - .createDevices = createDevices -}; diff --git a/Devices/lilygo-tdeck-max/Source/devices/Cst66xxTouch.cpp b/Devices/lilygo-tdeck-max/Source/devices/Cst66xxTouch.cpp deleted file mode 100644 index 5e0d48b94..000000000 --- a/Devices/lilygo-tdeck-max/Source/devices/Cst66xxTouch.cpp +++ /dev/null @@ -1,225 +0,0 @@ -#include "Cst66xxTouch.h" - -#include -#include -#include -#include - -#include - -// Touch reset is wired to XL9555 P07 (active low). -static constexpr uint32_t XL9555_PIN_TOUCH_RST = 7; - -constexpr auto* TAG = "CST66xx"; - -static constexpr TickType_t I2C_TIMEOUT = pdMS_TO_TICKS(20); - -// The protocol uses 4-byte register addresses, taken from the vendor's -// lib/HynTouch/src/hyn_cst66xx.c. - -// Normal-mode setup sequence (cst66xx_set_workmode, NOMAL_MODE). -static constexpr uint8_t CMD_DISABLE_LP_I2C[4] = {0xD0, 0x00, 0x04, 0x00}; -static constexpr uint8_t CMD_NORMAL_A[4] = {0xD0, 0x00, 0x00, 0x00}; -static constexpr uint8_t CMD_NORMAL_B[4] = {0xD0, 0x00, 0x0C, 0x00}; -static constexpr uint8_t CMD_NORMAL_C[4] = {0xD0, 0x00, 0x01, 0x00}; -// Read-point register: write these 4 bytes, then read the touch frame. -static constexpr uint8_t REG_READ_POINT[4] = {0xD0, 0x07, 0x00, 0x00}; -// Acknowledge/clear after each read. -static constexpr uint8_t CMD_ACK[4] = {0xD0, 0x00, 0x02, 0xAB}; -// Identity/config register (cst66xx_updata_tpinfo): read 50 bytes; buf[2]==0xCA -// && buf[3]==0xCA confirms a CST66xx. -static constexpr uint8_t REG_INFO[4] = {0xD0, 0x03, 0x00, 0x00}; - -static void setNormalMode(::Device* i2c, uint8_t address) { - i2c_controller_write(i2c, address, CMD_DISABLE_LP_I2C, sizeof(CMD_DISABLE_LP_I2C), I2C_TIMEOUT); - vTaskDelay(pdMS_TO_TICKS(1)); - i2c_controller_write(i2c, address, CMD_DISABLE_LP_I2C, sizeof(CMD_DISABLE_LP_I2C), I2C_TIMEOUT); - i2c_controller_write(i2c, address, CMD_NORMAL_A, sizeof(CMD_NORMAL_A), I2C_TIMEOUT); - i2c_controller_write(i2c, address, CMD_NORMAL_B, sizeof(CMD_NORMAL_B), I2C_TIMEOUT); - i2c_controller_write(i2c, address, CMD_NORMAL_C, sizeof(CMD_NORMAL_C), I2C_TIMEOUT); -} - -bool Cst66xxTouch::start() { - // Reset the controller (XL9555 P07, active low). The chip only re-initialises - // into normal reporting mode after a clean reset pulse. - auto* xl9555 = device_find_by_name("xl9555"); - if (xl9555 != nullptr) { - auto* rst = gpio_descriptor_acquire(xl9555, XL9555_PIN_TOUCH_RST, GPIO_OWNER_GPIO); - if (rst != nullptr) { - gpio_descriptor_set_flags(rst, GPIO_FLAG_DIRECTION_OUTPUT); - gpio_descriptor_set_level(rst, false); - vTaskDelay(pdMS_TO_TICKS(10)); - gpio_descriptor_set_level(rst, true); - gpio_descriptor_release(rst); - } - } - // The reset pulse exits boot mode; give the controller time to re-init. - vTaskDelay(pdMS_TO_TICKS(80)); - - // Put the controller into normal reporting mode and verify the identity. The - // first read after reset can miss, so retry like the vendor's - // cst66xx_updata_tpinfo (read 0xD0030000, expect buf[2]==buf[3]==0xCA). - auto* i2c = configuration.i2cController; - const uint8_t address = configuration.address; - bool confirmed = false; - for (int attempt = 0; attempt < 5 && !confirmed; ++attempt) { - setNormalMode(i2c, address); - uint8_t info[50] = {}; - if (i2c_controller_write(i2c, address, REG_INFO, sizeof(REG_INFO), I2C_TIMEOUT) == ERROR_NONE && - i2c_controller_read(i2c, address, info, sizeof(info), I2C_TIMEOUT) == ERROR_NONE && - info[2] == 0xCA && info[3] == 0xCA) { - confirmed = true; - } else { - vTaskDelay(pdMS_TO_TICKS(10)); - } - } - if (confirmed) { - LOG_I(TAG, "CST66xx initialised"); - } else { - LOG_W(TAG, "CST66xx identity not confirmed (continuing)"); - } - return true; -} - -bool Cst66xxTouch::stop() { - return true; -} - -bool Cst66xxTouch::readPoint(int16_t& x, int16_t& y) { - // CST66xx frame (cst66xx_report): write the read-point register, then read 9 - // bytes. buf[2]=report type (0xFF=position/key), buf[3] low nibble = finger - // count, high nibble = key count. The first 5-byte slot (buf[4..8]) is a key - // slot when key count > 0, otherwise the first finger; further fingers, if - // any, follow at buf[index+...]. - uint8_t buf[9] = {}; - error_t err = i2c_controller_write(configuration.i2cController, configuration.address, REG_READ_POINT, sizeof(REG_READ_POINT), I2C_TIMEOUT); - if (err == ERROR_NONE) { - err = i2c_controller_read(configuration.i2cController, configuration.address, buf, sizeof(buf), I2C_TIMEOUT); - } - if (err != ERROR_NONE) { - return false; - } - - // Acknowledge/clear the frame after every read. - i2c_controller_write(configuration.i2cController, configuration.address, CMD_ACK, sizeof(CMD_ACK), I2C_TIMEOUT); - - const uint8_t reportType = buf[2]; - const uint8_t fingerCount = buf[3] & 0x0F; - const uint8_t keyCount = (buf[3] & 0xF0) >> 4; - - // Bezel touch-keys are reported in the first slot (buf[8]): low nibble = key - // id, high nibble = state (non-zero = pressed). The controller reports keys - // mutually exclusively with finger coordinates, so a key frame never carries - // a touch point. - if (reportType == 0xFF && keyCount > 0) { - const uint8_t keyByte = buf[8]; - handleBezelKey(keyByte & 0x0F, (keyByte >> 4) != 0); - return false; - } - // No key in this frame: clear the latch so the next press fires once. - bezelKeyDown = false; - - if (reportType != 0xFF || fingerCount < 1) { - return false; - } - - // First finger's slot follows any key slots. - const uint8_t index = keyCount * 5; - const uint8_t event = buf[index + 8] >> 4; // 0 = up - if (event == 0) { - return false; - } - - int16_t rawX = static_cast(buf[index + 4] | (static_cast(buf[index + 7] & 0x0F) << 8)); - int16_t rawY = static_cast(buf[index + 5] | (static_cast(buf[index + 7] & 0xF0) << 4)); - - if (configuration.swapXy) { - std::swap(rawX, rawY); - } - if (configuration.mirrorX) { - rawX = static_cast(configuration.width - 1 - rawX); - } - if (configuration.mirrorY) { - rawY = static_cast(configuration.height - 1 - rawY); - } - - x = rawX; - y = rawY; - return true; -} - -// Bezel touch-key ids as reported by the CST66xx, left to right (confirmed on -// hardware). To re-map a button, change the action in the switch below — the -// mapping is also documented in NOTES.md. -static constexpr uint8_t BEZEL_KEY_HEART = 0; // left -static constexpr uint8_t BEZEL_KEY_SPEECH = 1; // centre -static constexpr uint8_t BEZEL_KEY_AIRPLANE = 2; // right - -void Cst66xxTouch::handleBezelKey(uint8_t keyId, bool pressed) { - if (!pressed) { - bezelKeyDown = false; - return; - } - if (bezelKeyDown) { - return; // still held; the press edge was already handled - } - bezelKeyDown = true; - - LOG_D(TAG, "bezel key %u", keyId); - - // Bezel button -> navigation action. Edit these cases to customise: - switch (keyId) { - case BEZEL_KEY_HEART: // Back: stop the current app (no-op at the launcher root) - tt::app::stop(); - break; - // Home (BEZEL_KEY_SPEECH) and Recents (BEZEL_KEY_AIRPLANE) are intentionally - // unbound: tt::app::start() always pushes a new instance onto the app - // stack, so repeated presses would keep growing it (Launcher/AppList - // instances never get cleaned up) and leak memory over time. The app - // stack has no API yet to collapse back to an existing instance instead - // of pushing a new one; wire these up once that exists. - default: - break; - } -} - -void Cst66xxTouch::readCallback(lv_indev_t* indev, lv_indev_data_t* data) { - auto* self = static_cast(lv_indev_get_user_data(indev)); - - int16_t x; - int16_t y; - if (self->readPoint(x, y)) { - self->lastX = x; - self->lastY = y; - data->point.x = x; - data->point.y = y; - data->state = LV_INDEV_STATE_PRESSED; - } else { - // Report the release at the last known position. - data->point.x = self->lastX; - data->point.y = self->lastY; - data->state = LV_INDEV_STATE_RELEASED; - } -} - -bool Cst66xxTouch::startLvgl(lv_display_t* display) { - if (indev != nullptr) { - return true; - } - - indev = lv_indev_create(); - lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER); - lv_indev_set_read_cb(indev, &readCallback); - lv_indev_set_display(indev, display); - lv_indev_set_user_data(indev, this); - return true; -} - -bool Cst66xxTouch::stopLvgl() { - if (indev == nullptr) { - return true; - } - lv_indev_delete(indev); - indev = nullptr; - return true; -} diff --git a/Devices/lilygo-tdeck-max/Source/devices/Cst66xxTouch.h b/Devices/lilygo-tdeck-max/Source/devices/Cst66xxTouch.h deleted file mode 100644 index 5a17fb04f..000000000 --- a/Devices/lilygo-tdeck-max/Source/devices/Cst66xxTouch.h +++ /dev/null @@ -1,54 +0,0 @@ -#pragma once - -#include -#include - -// Capacitive touch for the LilyGO T-Deck Max's Hynitron CST66xx controller at -// I2C 0x1A. (The vendor docs label it "CST328", but the vendor HynTouch driver -// probes cst66xx first and that is what answers here.) This is a minimal, -// single-touch port of the vendor's hyn_cst66xx.c protocol for LVGL pointer input. -class Cst66xxTouch final : public tt::hal::touch::TouchDevice { - -public: - - struct Configuration { - ::Device* i2cController; - uint8_t address; - uint16_t width; - uint16_t height; - bool swapXy; - bool mirrorX; - bool mirrorY; - }; - - explicit Cst66xxTouch(const Configuration& configuration) : configuration(configuration) {} - - std::string getName() const override { return "CST66xx"; } - std::string getDescription() const override { return "CST66xx I2C capacitive touch"; } - - bool start() override; - bool stop() override; - - bool supportsLvgl() const override { return true; } - bool startLvgl(lv_display_t* display) override; - bool stopLvgl() override; - lv_indev_t* getLvglIndev() override { return indev; } - - bool supportsTouchDriver() override { return false; } - std::shared_ptr getTouchDriver() override { return nullptr; } - -private: - - Configuration configuration; - lv_indev_t* indev = nullptr; - int16_t lastX = 0; - int16_t lastY = 0; - // The CST66xx also reports the three bezel touch-keys (heart / speech-bubble / - // paper-airplane). bezelKeyDown latches a press so we act once on the rising - // edge rather than every poll while the key is held. - bool bezelKeyDown = false; - - bool readPoint(int16_t& x, int16_t& y); - void handleBezelKey(uint8_t keyId, bool pressed); - static void readCallback(lv_indev_t* indev, lv_indev_data_t* data); -}; diff --git a/Devices/lilygo-tdeck-max/Source/devices/Display.cpp b/Devices/lilygo-tdeck-max/Source/devices/Display.cpp deleted file mode 100644 index 2b9e99506..000000000 --- a/Devices/lilygo-tdeck-max/Source/devices/Display.cpp +++ /dev/null @@ -1,57 +0,0 @@ -#include "Display.h" -#include "Cst66xxTouch.h" - -#include -#include -#include - -constexpr auto* TAG = "TdeckmaxDisplay"; - -// Pins and I2C address from Xinyuan-LilyGO/T-Deck-MAX's -// lib/TDeckMaxBoard/src/TDeckMaxBoard.h and docs/pinmap.md. -constexpr auto EPD_SPI_HOST = SPI2_HOST; -constexpr auto EPD_PIN_CS = GPIO_NUM_34; -constexpr auto EPD_PIN_DC = GPIO_NUM_35; -constexpr auto EPD_PIN_RST = GPIO_NUM_9; -constexpr auto EPD_PIN_BUSY = GPIO_NUM_37; -constexpr uint16_t TOUCH_I2C_ADDRESS = 0x1A; - -static std::shared_ptr createTouch() { - auto* i2c = device_find_by_name("i2c0"); - if (i2c == nullptr) { - LOG_E(TAG, "i2c0 not found, booting without touch"); - return nullptr; - } - - // The touch reset line is released earlier via the XL9555 expander (see - // Configuration.cpp). Orientation flags are starting guesses; adjust after - // checking on hardware. - const Cst66xxTouch::Configuration configuration = { - .i2cController = i2c, - .address = TOUCH_I2C_ADDRESS, - .width = Gdeq031t10Display::WIDTH, - .height = Gdeq031t10Display::HEIGHT, - .swapXy = false, - .mirrorX = false, - .mirrorY = false, - }; - - return std::make_shared(configuration); -} - -std::shared_ptr createDisplay() { - auto configuration = std::make_unique( - EPD_SPI_HOST, - EPD_PIN_CS, - EPD_PIN_DC, - EPD_PIN_RST, - EPD_PIN_BUSY, - createTouch(), - 10'000'000, - // Default to a fast (~1s) refresh for the automatic ghost-clears so the - // full-screen flash they cause is as short as possible. - Gdeq031t10Display::RefreshMode::Fast - ); - - return std::make_shared(std::move(configuration)); -} diff --git a/Devices/lilygo-tdeck-max/Source/devices/Display.h b/Devices/lilygo-tdeck-max/Source/devices/Display.h deleted file mode 100644 index 7a9b967d1..000000000 --- a/Devices/lilygo-tdeck-max/Source/devices/Display.h +++ /dev/null @@ -1,5 +0,0 @@ -#pragma once - -#include - -std::shared_ptr createDisplay(); diff --git a/Devices/lilygo-tdeck-max/Source/devices/TdeckmaxKeyboard.cpp b/Devices/lilygo-tdeck-max/Source/devices/TdeckmaxKeyboard.cpp deleted file mode 100644 index e40b6a604..000000000 --- a/Devices/lilygo-tdeck-max/Source/devices/TdeckmaxKeyboard.cpp +++ /dev/null @@ -1,249 +0,0 @@ -#include "TdeckmaxKeyboard.h" - -#include - -#include -#include - -constexpr auto* TAG = "TdeckmaxKeyboard"; - -// Keyboard backlight (LED_PWM), GPIO42 on the T-Deck Max. -constexpr auto BACKLIGHT = GPIO_NUM_42; - -constexpr auto KB_ROWS = 4; -constexpr auto KB_COLS = 10; - -// Keymaps are written in the vendor's row/column orientation -// (Xinyuan-LilyGO/T-Deck-MAX examples/factory/peri_keypad.cpp). The TCA8418 -// columns are wired in reverse, so the vendor reads keymap[row][9 - col]. We do -// the same reversal in processKeyboard(), which keeps these tables a direct, -// verifiable copy of the vendor layout. -// -// Modifier keys (ALT and SYM) are blanked here ('\0') and handled by position: -// ALT -> shift/uppercase, at vendor column 0 of row 2 -// SYM -> symbol layer, at vendor column 8 of row 3 - -// Lowercase (base) layer -static constexpr char keymap_lc[KB_ROWS][KB_COLS] = { - {'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p'}, - {'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', LV_KEY_BACKSPACE}, - {'\0', 'z', 'x', 'c', 'v', 'b', 'n', 'm', '$', LV_KEY_ENTER}, - {'\0', '\0', '\0', '\0', '\0', LV_KEY_PREV, '0', ' ', '\0', LV_KEY_NEXT} -}; - -// Uppercase layer (ALT held or caps toggled) -static constexpr char keymap_uc[KB_ROWS][KB_COLS] = { - {'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P'}, - {'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', LV_KEY_BACKSPACE}, - {'\0', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', '$', LV_KEY_ENTER}, - {'\0', '\0', '\0', '\0', '\0', LV_KEY_PREV, '0', ' ', '\0', LV_KEY_NEXT} -}; - -// Symbol layer (SYM held). The T-Deck Max silkscreen for the symbol layer is -// not documented in the vendor sources, so these are sensible defaults; adjust -// to match the printed keys once verified on hardware. -static constexpr char keymap_sy[KB_ROWS][KB_COLS] = { - {'1', '2', '3', '4', '5', '6', '7', '8', '9', '0'}, - {'@', '#', '+', '-', '*', '/', '(', ')', '_', LV_KEY_BACKSPACE}, - {'\0', '!', '?', ';', ':', '\'', '"', ',', '.', LV_KEY_ENTER}, - {'\0', '\0', '\0', '\0', '\0', LV_KEY_PREV, '0', ' ', '\0', LV_KEY_NEXT} -}; - -void TdeckmaxKeyboard::readCallback(lv_indev_t* indev, lv_indev_data_t* data) { - auto keyboard = static_cast(lv_indev_get_user_data(indev)); - - // Emit the release edge for the previous key first: LVGL's keypad handling - // only delivers a key on a RELEASED->PRESSED transition, so two different - // keys reported PRESSED back-to-back would swallow the second one. - if (keyboard->lastKeyNeedsRelease) { - keyboard->lastKeyNeedsRelease = false; - data->key = keyboard->lastKey; - data->state = LV_INDEV_STATE_RELEASED; - data->continue_reading = uxQueueMessagesWaiting(keyboard->queue) > 0; - return; - } - - char keypress = 0; - if (xQueueReceive(keyboard->queue, &keypress, 0) == pdPASS) { - keyboard->lastKey = static_cast(keypress); - keyboard->lastKeyNeedsRelease = true; - data->key = keyboard->lastKey; - data->state = LV_INDEV_STATE_PRESSED; - // Drain the whole queue in this read cycle so a typing burst lands in - // one render pass (and thus a single e-paper refresh). - data->continue_reading = true; - } else { - data->key = 0; - data->state = LV_INDEV_STATE_RELEASED; - } -} - -void TdeckmaxKeyboard::processKeyboard() { - bool anykey_pressed = false; - - // Each update() pops one event from the TCA8418's FIFO. Drain it fully per - // poll (bounded, in case of a misbehaving bus) so a fast typing burst isn't - // throttled to one event per poll period. Handling each event as a discrete - // press/release edge also means a key held across another key's press (fast - // typing rollover) no longer re-sends its character. - for (int drained = 0; drained < 16 && keypad->update(); drained++) { - if (keypad->released_key_count > 0) { - // Release event: only modifier state cares about releases. - auto row = keypad->released_list[0].row; - auto vcol = (KB_COLS - 1) - keypad->released_list[0].col; - - if ((row == 2) && (vcol == 0)) { - shiftPressed = false; // ALT key - } - if ((row == 3) && (vcol == 8)) { - symPressed = false; // SYM key - } - } else if (keypad->pressed_key_count > 0) { - // Press event: the key that caused it is the newest list entry. - auto row = keypad->pressed_list[keypad->pressed_key_count - 1].row; - auto vcol = (KB_COLS - 1) - keypad->pressed_list[keypad->pressed_key_count - 1].col; - anykey_pressed = true; - - if ((row == 2) && (vcol == 0)) { - shiftPressed = true; // ALT key - } else if ((row == 3) && (vcol == 8)) { - symPressed = true; // SYM key - } else { - char chr; - if (symPressed) { - chr = keymap_sy[row][vcol]; - } else if (shiftPressed || capToggle) { - chr = keymap_uc[row][vcol]; - } else { - chr = keymap_lc[row][vcol]; - } - - // Non-blocking: this runs on the periodic input timer, so a full - // queue (reader stalled) must drop the keystroke rather than - // stall this callback and the timer's other periodic work. - if (chr != '\0' && xQueueSend(queue, &chr, 0) != pdPASS) { - LOG_W(TAG, "Keyboard queue full, dropping keystroke"); - } - } - - if ((symPressed && shiftPressed) && capToggleArmed) { - capToggle = !capToggle; - capToggleArmed = false; - } - } - - if ((!symPressed && !shiftPressed) && !capToggleArmed) { - capToggleArmed = true; - } - } - - if (anykey_pressed) { - makeBacklightImpulse(); - } -} - -bool TdeckmaxKeyboard::startLvgl(lv_display_t* display) { - backlightOkay = initBacklight(BACKLIGHT, 30000, LEDC_TIMER_0, LEDC_CHANNEL_1); - keypad->init(KB_ROWS, KB_COLS); - - assert(inputTimer == nullptr); - inputTimer = std::make_unique(tt::Timer::Type::Periodic, tt::kernel::millisToTicks(20), [this] { - processKeyboard(); - }); - - assert(backlightImpulseTimer == nullptr); - backlightImpulseTimer = std::make_unique(tt::Timer::Type::Periodic, tt::kernel::millisToTicks(50), [this] { - processBacklightImpulse(); - }); - - 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); - - inputTimer->start(); - backlightImpulseTimer->start(); - - return true; -} - -bool TdeckmaxKeyboard::stopLvgl() { - assert(inputTimer); - inputTimer->stop(); - inputTimer = nullptr; - - assert(backlightImpulseTimer); - backlightImpulseTimer->stop(); - backlightImpulseTimer = nullptr; - - lv_indev_delete(kbHandle); - kbHandle = nullptr; - return true; -} - -bool TdeckmaxKeyboard::isAttached() const { - return i2c_controller_has_device_at_address(keypad->getController(), keypad->getAddress(), 100) == ERROR_NONE; -} - -bool TdeckmaxKeyboard::initBacklight(gpio_num_t pin, uint32_t frequencyHz, ledc_timer_t timer, ledc_channel_t channel) { - backlightPin = pin; - backlightTimer = timer; - backlightChannel = channel; - - ledc_timer_config_t ledc_timer = { - .speed_mode = LEDC_LOW_SPEED_MODE, - .duty_resolution = LEDC_TIMER_8_BIT, - .timer_num = backlightTimer, - .freq_hz = frequencyHz, - .clk_cfg = LEDC_AUTO_CLK, - .deconfigure = false - }; - - if (ledc_timer_config(&ledc_timer) != ESP_OK) { - LOG_E(TAG, "Backlight timer config failed"); - return false; - } - - ledc_channel_config_t ledc_channel = { - .gpio_num = backlightPin, - .speed_mode = LEDC_LOW_SPEED_MODE, - .channel = backlightChannel, - .intr_type = LEDC_INTR_DISABLE, - .timer_sel = backlightTimer, - .duty = 0, - .hpoint = 0, - .sleep_mode = LEDC_SLEEP_MODE_NO_ALIVE_NO_PD, - .flags = { - .output_invert = 0 - } - }; - - if (ledc_channel_config(&ledc_channel) != ESP_OK) { - LOG_E(TAG, "Backlight channel config failed"); - return false; - } - - return true; -} - -bool TdeckmaxKeyboard::setBacklightDuty(uint8_t duty) { - if (!backlightOkay) { - LOG_E(TAG, "Backlight not ready"); - return false; - } - return (ledc_set_duty(LEDC_LOW_SPEED_MODE, backlightChannel, duty) == ESP_OK) && - (ledc_update_duty(LEDC_LOW_SPEED_MODE, backlightChannel) == ESP_OK); -} - -void TdeckmaxKeyboard::makeBacklightImpulse() { - backlightImpulseDuty = 255; - setBacklightDuty(backlightImpulseDuty); -} - -void TdeckmaxKeyboard::processBacklightImpulse() { - if (backlightImpulseDuty > 0) { - backlightImpulseDuty--; - setBacklightDuty(backlightImpulseDuty); - } -} diff --git a/Devices/lilygo-tdeck-max/Source/devices/TdeckmaxKeyboard.h b/Devices/lilygo-tdeck-max/Source/devices/TdeckmaxKeyboard.h deleted file mode 100644 index 4ff9079a4..000000000 --- a/Devices/lilygo-tdeck-max/Source/devices/TdeckmaxKeyboard.h +++ /dev/null @@ -1,69 +0,0 @@ -#pragma once - -#include -#include - -#include -#include -#include -#include - -// QWERTY keyboard for the LilyGO T-Deck (Pro) Max. -// -// The keyboard is a TCA8418 4x10 matrix scanner on the shared I2C bus at 0x34. -// The keyboard reset line is wired through the XL9555 IO expander (P11), so it -// must be released before this device is started (see Configuration.cpp). -// -// Pins, address and keymap are taken from Xinyuan-LilyGO/T-Deck-MAX: -// examples/factory/peri_keypad.cpp and lib/TDeckMaxBoard/src/TDeckMaxBoard.h. -class TdeckmaxKeyboard final : public tt::hal::keyboard::KeyboardDevice { - - lv_indev_t* kbHandle = nullptr; - gpio_num_t backlightPin = GPIO_NUM_NC; - ledc_timer_t backlightTimer; - ledc_channel_t backlightChannel; - bool backlightOkay = false; - int backlightImpulseDuty = 0; - QueueHandle_t queue = nullptr; - // LVGL only registers a key on a RELEASED->PRESSED edge, so readCallback - // alternates press/release per queued key; this tracks the pending release. - uint32_t lastKey = 0; - bool lastKeyNeedsRelease = false; - - std::shared_ptr keypad; - std::unique_ptr inputTimer; - std::unique_ptr backlightImpulseTimer; - - bool shiftPressed = false; - bool symPressed = false; - bool capToggle = false; - bool capToggleArmed = true; - - bool initBacklight(gpio_num_t pin, uint32_t frequencyHz, ledc_timer_t timer, ledc_channel_t channel); - void processKeyboard(); - void processBacklightImpulse(); - - static void readCallback(lv_indev_t* indev, lv_indev_data_t* data); - -public: - - explicit TdeckmaxKeyboard(const std::shared_ptr& tca) : keypad(tca) { - queue = xQueueCreate(20, sizeof(char)); - } - - ~TdeckmaxKeyboard() override { - vQueueDelete(queue); - } - - std::string getName() const override { return "T-Deck Max Keyboard"; } - std::string getDescription() const override { return "T-Deck Max TCA8418 I2C matrix keyboard"; } - - bool startLvgl(lv_display_t* display) override; - bool stopLvgl() override; - - bool isAttached() const override; - lv_indev_t* getLvglIndev() override { return kbHandle; } - - bool setBacklightDuty(uint8_t duty); - void makeBacklightImpulse(); -}; diff --git a/Devices/lilygo-tdeck-max/Source/devices/TdeckmaxPower.cpp b/Devices/lilygo-tdeck-max/Source/devices/TdeckmaxPower.cpp deleted file mode 100644 index dd42ead5a..000000000 --- a/Devices/lilygo-tdeck-max/Source/devices/TdeckmaxPower.cpp +++ /dev/null @@ -1,139 +0,0 @@ -#include "TdeckmaxPower.h" - -#include -#include - -constexpr auto* TAG = "TdeckmaxPower"; - -// SY6970 charger (newer T-Deck Max revisions; older boards use BQ25896 @ 0x6B). -// Register map is BQ25896-compatible for everything used here (see NOTES.md). -static constexpr uint8_t SY6970_ADDRESS = 0x6A; -static constexpr uint8_t SY6970_REG_03 = 0x03; // bit4 CHG_CONFIG: charge enable -static constexpr uint8_t SY6970_REG_04 = 0x04; // bits[6:0] ICHG: charge current, 64 mA/LSB -static constexpr uint8_t SY6970_REG_06 = 0x06; // bits[7:2] VREG: charge voltage, 3840 mV + n*16 mV -static constexpr uint8_t SY6970_REG_07 = 0x07; // bits[5:4] WATCHDOG: I2C watchdog timer -static constexpr uint8_t SY6970_REG_09 = 0x09; -static constexpr uint8_t SY6970_REG_0B = 0x0B; // read-only status -static constexpr uint8_t SY6970_CHG_CONFIG = 1 << 4; -static constexpr uint8_t SY6970_BATFET_DIS = 1 << 5; // Force BATFET off = ship mode - -static constexpr TickType_t I2C_TIMEOUT = pdMS_TO_TICKS(50); - -// Charge parameters from the vendor factory firmware (XPowersLib SY6970 init): -// watchdog off, 4288 mV target, 1024 mA fast-charge current. The I2C watchdog -// must be disabled first, otherwise the chip reverts custom register values to -// defaults after the watchdog expires. Input current limit (REG00) is left at -// the hardware PSEL/ILIM default. Non-fatal: charging still works on chip -// defaults if any write fails. -bool TdeckmaxPower::configureCharger() const { - if (i2c_controller_register8_reset_bits(i2c, SY6970_ADDRESS, SY6970_REG_07, 0x30, I2C_TIMEOUT) != ERROR_NONE) { - LOG_E(TAG, "Failed to disable SY6970 watchdog"); - return false; - } - - uint8_t reg06 = 0; - if (i2c_controller_register8_get(i2c, SY6970_ADDRESS, SY6970_REG_06, ®06, I2C_TIMEOUT) != ERROR_NONE || - i2c_controller_register8_set(i2c, SY6970_ADDRESS, SY6970_REG_06, (reg06 & 0x03) | (28 << 2), I2C_TIMEOUT) != ERROR_NONE) { // 3840 + 28*16 = 4288 mV - LOG_E(TAG, "Failed to set SY6970 charge voltage"); - return false; - } - - uint8_t reg04 = 0; - if (i2c_controller_register8_get(i2c, SY6970_ADDRESS, SY6970_REG_04, ®04, I2C_TIMEOUT) != ERROR_NONE || - i2c_controller_register8_set(i2c, SY6970_ADDRESS, SY6970_REG_04, (reg04 & 0x80) | 0x10, I2C_TIMEOUT) != ERROR_NONE) { // 16 * 64 = 1024 mA - LOG_E(TAG, "Failed to set SY6970 charge current"); - return false; - } - - LOG_I(TAG, "SY6970 configured: 4288 mV / 1024 mA, watchdog off"); - return true; -} - -bool TdeckmaxPower::isAllowedToCharge() const { - uint8_t reg03 = 0; - if (i2c_controller_register8_get(i2c, SY6970_ADDRESS, SY6970_REG_03, ®03, I2C_TIMEOUT) != ERROR_NONE) { - return false; - } - return (reg03 & SY6970_CHG_CONFIG) != 0; -} - -void TdeckmaxPower::setAllowedToCharge(bool canCharge) { - error_t result; - if (canCharge) { - result = i2c_controller_register8_set_bits(i2c, SY6970_ADDRESS, SY6970_REG_03, SY6970_CHG_CONFIG, I2C_TIMEOUT); - } else { - result = i2c_controller_register8_reset_bits(i2c, SY6970_ADDRESS, SY6970_REG_03, SY6970_CHG_CONFIG, I2C_TIMEOUT); - } - if (result != ERROR_NONE) { - LOG_E(TAG, "Failed to set SY6970 charge enable"); - } -} - -bool TdeckmaxPower::supportsMetric(MetricType type) const { - switch (type) { - using enum MetricType; - case IsCharging: - case Current: - case BatteryVoltage: - case ChargeLevel: - return true; - default: - return false; - } -} - -bool TdeckmaxPower::getMetric(MetricType type, MetricData& data) { - uint16_t u16 = 0; - int16_t s16 = 0; - switch (type) { - using enum MetricType; - case IsCharging: { - // REG0B CHRG_STAT bits[4:3]: 0 = not charging, 1 = pre-charge, - // 2 = fast charge, 3 = charge done. Only 1|2 count as charging — - // "done" must not (XPowersLib had this inverted once). Fall back to - // the fuel gauge's discharge flag if the charger doesn't answer. - uint8_t reg0b = 0; - if (i2c_controller_register8_get(i2c, SY6970_ADDRESS, SY6970_REG_0B, ®0b, I2C_TIMEOUT) == ERROR_NONE) { - uint8_t charge_status = (reg0b >> 3) & 0x03; - data.valueAsBool = charge_status == 1 || charge_status == 2; - return true; - } - Bq27220::BatteryStatus status; - if (gauge->getBatteryStatus(status)) { - data.valueAsBool = !status.reg.DSG; - return true; - } - return false; - } - case Current: - if (gauge->getCurrent(s16)) { - data.valueAsInt32 = s16; - return true; - } - return false; - case BatteryVoltage: - if (gauge->getVoltage(u16)) { - data.valueAsUint32 = u16; - return true; - } - return false; - case ChargeLevel: - if (gauge->getStateOfCharge(u16)) { - data.valueAsUint8 = u16; - return true; - } - return false; - default: - return false; - } -} - -void TdeckmaxPower::powerOff() { - // Ship mode: force the charger's BATFET off (REG09 bit5). Mirrors the vendor - // XPowersLib PowersSY6970::shutdown(). Note: this only fully powers the board - // down when running on battery with USB unplugged. - LOG_I(TAG, "Power off (SY6970 BATFET_DIS)"); - if (i2c_controller_register8_set_bits(i2c, SY6970_ADDRESS, SY6970_REG_09, SY6970_BATFET_DIS, pdMS_TO_TICKS(50)) != ERROR_NONE) { - LOG_E(TAG, "Failed to write SY6970 shutdown register"); - } -} diff --git a/Devices/lilygo-tdeck-max/Source/devices/TdeckmaxPower.h b/Devices/lilygo-tdeck-max/Source/devices/TdeckmaxPower.h deleted file mode 100644 index b308e656c..000000000 --- a/Devices/lilygo-tdeck-max/Source/devices/TdeckmaxPower.h +++ /dev/null @@ -1,42 +0,0 @@ -#pragma once - -#include "Tactility/hal/power/PowerDevice.h" - -#include -#include - -#include - -using tt::hal::power::PowerDevice; - -// Power device for the LilyGO T-Deck Max. Battery metrics come from the BQ27220 -// fuel gauge (I2C 0x55); charging status, charge control and power-off (ship -// mode, REG09 BATFET_DIS) use the SY6970 charger. Newer T-Deck Max revisions -// ship the SY6970 (0x6A) instead of the older BQ25896 (0x6B); this board has -// the SY6970 (confirmed by I2C scan). The two are register-compatible for -// everything used here. -class TdeckmaxPower final : public PowerDevice { - std::shared_ptr gauge; - ::Device* i2c; // i2c0 controller, used to reach the SY6970 charger - - bool configureCharger() const; - -public: - - TdeckmaxPower(const std::shared_ptr& gauge, ::Device* i2c) : gauge(gauge), i2c(i2c) { - configureCharger(); - } - - std::string getName() const final { return "T-Deck Max Power"; } - std::string getDescription() const final { return "Battery metrics (BQ27220), SY6970 charging and power-off"; } - - bool supportsMetric(MetricType type) const override; - bool getMetric(MetricType type, MetricData& data) override; - - bool supportsChargeControl() const override { return true; } - bool isAllowedToCharge() const override; - void setAllowedToCharge(bool canCharge) override; - - bool supportsPowerOff() const override { return true; } - void powerOff() override; -}; diff --git a/Devices/lilygo-tdeck-max/devicetree.yaml b/Devices/lilygo-tdeck-max/devicetree.yaml index 07314c274..cf7b192a9 100644 --- a/Devices/lilygo-tdeck-max/devicetree.yaml +++ b/Devices/lilygo-tdeck-max/devicetree.yaml @@ -1,4 +1,8 @@ dependencies: - Platforms/platform-esp32 - Drivers/xl9555-module + - Drivers/cst66xx-module + - Drivers/tca8418-module + - Drivers/sy6970-module + - Drivers/bq27220-module dts: lilygo,tdeck-max.dts diff --git a/Devices/lilygo-tdeck-max/lilygo,tdeck-max.dts b/Devices/lilygo-tdeck-max/lilygo,tdeck-max.dts index 06dbb9db2..46405345b 100644 --- a/Devices/lilygo-tdeck-max/lilygo,tdeck-max.dts +++ b/Devices/lilygo-tdeck-max/lilygo,tdeck-max.dts @@ -7,7 +7,13 @@ #include #include #include +#include +#include #include +#include +#include +#include +#include // Reference: https://github.com/Xinyuan-LilyGO/T-Deck-MAX // lib/TDeckMaxBoard/src/TDeckMaxBoard.h, docs/pinmap.md @@ -40,6 +46,88 @@ compatible = "xlsemi,xl9555"; reg = <0x20>; }; + + // Vendor docs label this chip "CST328", but the vendor HynTouch driver probes + // cst66xx first and that is what answers on this board. + touch { + compatible = "hynitron,cst66xx"; + reg = <0x1A>; + x-max = <240>; + y-max = <320>; + // Reset routed through the XL9555 IO expander (P07, active low). + pin-reset = <&xl9555 7 GPIO_FLAG_NONE>; + }; + + // TCA8418 4x10 matrix scanner. Keymaps are written in the vendor's row/column + // orientation (Xinyuan-LilyGO/T-Deck-MAX examples/factory/peri_keypad.cpp); the + // TCA8418 columns are wired in reverse of that order, hence reverse-columns. + // ALT (shift/uppercase) is row 2 col 0; SYM (symbol layer) is row 3 col 8. + keyboard { + compatible = "ti,tca8418"; + reg = <0x34>; + rows = <4>; + columns = <10>; + reverse-columns; + keymap-lc = [ + 113 119 101 114 116 121 117 105 111 112 // q w e r t y u i o p + 97 115 100 102 103 104 106 107 108 8 // a s d f g h j k l BACKSPACE + 0 122 120 99 118 98 110 109 36 10 // z x c v b n m $ ENTER + 0 0 0 0 0 11 48 32 0 9 // PREV 0 SPC NEXT + ]; + keymap-uc = [ + 81 87 69 82 84 89 85 73 79 80 // Q W E R T Y U I O P + 65 83 68 70 71 72 74 75 76 8 // A S D F G H J K L BACKSPACE + 0 90 88 67 86 66 78 77 36 10 // Z X C V B N M $ ENTER + 0 0 0 0 0 11 48 32 0 9 // PREV 0 SPC NEXT + ]; + keymap-sy = [ + 49 50 51 52 53 54 55 56 57 48 // 1 2 3 4 5 6 7 8 9 0 + 64 35 43 45 42 47 40 41 95 8 // @ # + - * / ( ) _ BACKSPACE + 0 33 63 59 58 39 34 44 46 10 // ! ? ; : ' " , . ENTER + 0 0 0 0 0 11 48 32 0 9 // PREV 0 SPC NEXT + ]; + shift-row = <2>; + shift-col = <0>; + sym-row = <3>; + sym-col = <8>; + // Reset routed through the XL9555 IO expander (P11, active low). + pin-reset = <&xl9555 9 GPIO_FLAG_NONE>; + }; + + // Battery metrics from the BQ27220 fuel gauge; power-off via the SY6970 charger. + // Newer T-Deck Max revisions ship the SY6970 (0x6A) instead of the older BQ25896 + // (0x6B); this board has the SY6970 (confirmed by I2C scan). The two are + // register-compatible for everything used here. + sy6970 { + compatible = "silergy,sy6970"; + reg = <0x6A>; + charge-voltage-mv = <4288>; + charge-current-ma = <1024>; + }; + + bq27220 { + compatible = "ti,bq27220"; + reg = <0x55>; + }; + }; + + // Keyboard backlight (LED_PWM), GPIO42. The keypress-triggered flash/decay effect + // from the pre-migration driver isn't reproduced here - kernel drivers don't have a + // home for that kind of UI policy (same reasoning as the touch driver's dropped bezel + // keys). Off by default; app code can drive brightness via the generic pwm-backlight + // API if desired. + keyboard_backlight_pwm { + compatible = "espressif,esp32-pwm-ledc"; + pin = <&gpio0 42 GPIO_FLAG_NONE>; + period-ns = <33333>; + ledc-timer = <0>; + ledc-channel = <0>; + }; + + keyboard_backlight { + compatible = "pwm-backlight"; + status = "disabled"; + pwm = <&keyboard_backlight_pwm>; }; spi0 { diff --git a/Devices/lilygo-tdeck-max/source/Configuration.cpp b/Devices/lilygo-tdeck-max/source/Configuration.cpp new file mode 100644 index 000000000..f4f0f0d98 --- /dev/null +++ b/Devices/lilygo-tdeck-max/source/Configuration.cpp @@ -0,0 +1,35 @@ +#include "devices/Display.h" + +#include +#include +#include +#include + +using namespace tt::hal; + +static bool initBoot() { + // The LoRa and SD chip-selects share the EPD's SPI bus but aren't wired up + // yet. spi0's start() already drives every cs-gpio high during kernel init; + // re-assert deselection before the EPD driver first transacts, as a cheap + // guard against either chip latching stray EPD command bytes (same pattern + // as the esp32_sdspi mount path). + auto* spi0 = device_find_by_name("spi0"); + check(spi0 != nullptr); + esp32_spi_deselect_all_cs(spi0); + + return true; +} + +static DeviceVector createDevices() { + // Touch, keyboard and battery/charging are kernel drivers (cst66xx, tca8418, sy6970, + // bq27220 - see the .dts), started independently of this HAL layer. Only the EPD + // display remains on the deprecated HAL. + return DeviceVector { + createDisplay() + }; +} + +extern const Configuration hardwareConfiguration = { + .initBoot = initBoot, + .createDevices = createDevices +}; diff --git a/Devices/lilygo-tdeck-max/source/devices/Display.cpp b/Devices/lilygo-tdeck-max/source/devices/Display.cpp new file mode 100644 index 000000000..ffc7ba783 --- /dev/null +++ b/Devices/lilygo-tdeck-max/source/devices/Display.cpp @@ -0,0 +1,29 @@ +#include "Display.h" + +#include + +// Pins from Xinyuan-LilyGO/T-Deck-MAX's lib/TDeckMaxBoard/src/TDeckMaxBoard.h and docs/pinmap.md. +constexpr auto EPD_SPI_HOST = SPI2_HOST; +constexpr auto EPD_PIN_CS = GPIO_NUM_34; +constexpr auto EPD_PIN_DC = GPIO_NUM_35; +constexpr auto EPD_PIN_RST = GPIO_NUM_9; +constexpr auto EPD_PIN_BUSY = GPIO_NUM_37; + +std::shared_ptr createDisplay() { + // Touch is a kernel driver (hynitron,cst66xx - see the .dts), discovered and fed to LVGL + // independently of this display, so no touch device is passed into the configuration here. + auto configuration = std::make_unique( + EPD_SPI_HOST, + EPD_PIN_CS, + EPD_PIN_DC, + EPD_PIN_RST, + EPD_PIN_BUSY, + nullptr, + 10'000'000, + // Default to a fast (~1s) refresh for the automatic ghost-clears so the + // full-screen flash they cause is as short as possible. + Gdeq031t10Display::RefreshMode::Fast + ); + + return std::make_shared(std::move(configuration)); +} diff --git a/Devices/guition-jc1060p470ciwy/Source/devices/Display.h b/Devices/lilygo-tdeck-max/source/devices/Display.h similarity index 100% rename from Devices/guition-jc1060p470ciwy/Source/devices/Display.h rename to Devices/lilygo-tdeck-max/source/devices/Display.h diff --git a/Devices/lilygo-tdeck-max/Source/module.cpp b/Devices/lilygo-tdeck-max/source/module.cpp similarity index 100% rename from Devices/lilygo-tdeck-max/Source/module.cpp rename to Devices/lilygo-tdeck-max/source/module.cpp diff --git a/Devices/lilygo-tdisplay-s3/CMakeLists.txt b/Devices/lilygo-tdisplay-s3/CMakeLists.txt index 3029aa07e..a832a83a2 100644 --- a/Devices/lilygo-tdisplay-s3/CMakeLists.txt +++ b/Devices/lilygo-tdisplay-s3/CMakeLists.txt @@ -1,7 +1,6 @@ -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 ButtonControl PwmBacklight EstimatedPower ST7789-i8080 + REQUIRES TactilityKernel driver ) diff --git a/Devices/lilygo-tdisplay-s3/Source/Configuration.cpp b/Devices/lilygo-tdisplay-s3/Source/Configuration.cpp deleted file mode 100644 index bc5b80549..000000000 --- a/Devices/lilygo-tdisplay-s3/Source/Configuration.cpp +++ /dev/null @@ -1,22 +0,0 @@ -#include "devices/Display.h" -#include "devices/Power.h" - -#include -#include - -bool initBoot(); - -using namespace tt::hal; - -static std::vector> createDevices() { - return { - createPower(), - createDisplay(), - ButtonControl::createTwoButtonControl(0, 14), - }; -} - -extern const Configuration hardwareConfiguration = { - .initBoot = initBoot, - .createDevices = createDevices -}; diff --git a/Devices/lilygo-tdisplay-s3/Source/Init.cpp b/Devices/lilygo-tdisplay-s3/Source/Init.cpp deleted file mode 100644 index e01fb78bd..000000000 --- a/Devices/lilygo-tdisplay-s3/Source/Init.cpp +++ /dev/null @@ -1,44 +0,0 @@ -#include "devices/Power.h" -#include "devices/Display.h" - -#include "PwmBacklight.h" -#include -#include -#include - -#define TAG "tdisplay-s3" - -static bool powerOn() { - gpio_config_t power_signal_config = { - .pin_bit_mask = BIT64(TDISPLAY_S3_POWERON_GPIO), - .mode = GPIO_MODE_OUTPUT, - .pull_up_en = GPIO_PULLUP_DISABLE, - .pull_down_en = GPIO_PULLDOWN_DISABLE, - .intr_type = GPIO_INTR_DISABLE, - }; - - if (gpio_config(&power_signal_config) != ESP_OK) { - return false; - } - - if (gpio_set_level(TDISPLAY_S3_POWERON_GPIO, 1) != ESP_OK) { - return false; - } - - return true; -} - -bool initBoot() { - LOG_I(TAG, "Powering on"); - if (!powerOn()) { - LOG_E(TAG, "Power on failed"); - return false; - } - - if (!driver::pwmbacklight::init(DISPLAY_BL, 30000)) { - LOG_E(TAG, "Backlight init failed"); - return false; - } - - return true; -} \ No newline at end of file diff --git a/Devices/lilygo-tdisplay-s3/Source/devices/Display.cpp b/Devices/lilygo-tdisplay-s3/Source/devices/Display.cpp deleted file mode 100644 index eeb902e63..000000000 --- a/Devices/lilygo-tdisplay-s3/Source/devices/Display.cpp +++ /dev/null @@ -1,30 +0,0 @@ -#include "Display.h" - -#include "St7789i8080Display.h" -#include "PwmBacklight.h" - -std::shared_ptr createDisplay() { - // Create configuration - auto config = St7789i8080Display::Configuration( - DISPLAY_CS, // CS - DISPLAY_DC, // DC - DISPLAY_WR, // WR - DISPLAY_RD, // RD - { DISPLAY_I80_D0, DISPLAY_I80_D1, DISPLAY_I80_D2, DISPLAY_I80_D3, - DISPLAY_I80_D4, DISPLAY_I80_D5, DISPLAY_I80_D6, DISPLAY_I80_D7 }, // D0..D7 - DISPLAY_RST, // RST - DISPLAY_BL // BL - ); - - // Set resolution explicitly - config.horizontalResolution = DISPLAY_HORIZONTAL_RESOLUTION; - config.verticalResolution = DISPLAY_VERTICAL_RESOLUTION; - config.backlightDutyFunction = driver::pwmbacklight::setBacklightDuty; - - // Adjust other settings as needed - config.gapX = 35; // ST7789 has a 35 pixel gap on X axis - config.pixelClockFrequency = 10 * 1000 * 1000; // 10MHz for better stability - - auto display = std::make_shared(config); - return display; -} diff --git a/Devices/lilygo-tdisplay-s3/Source/devices/Display.h b/Devices/lilygo-tdisplay-s3/Source/devices/Display.h deleted file mode 100644 index 4dbf9a6ce..000000000 --- a/Devices/lilygo-tdisplay-s3/Source/devices/Display.h +++ /dev/null @@ -1,26 +0,0 @@ -#pragma once - -#include -#include - -class St7789i8080Display; - -constexpr auto DISPLAY_CS = GPIO_NUM_6; -constexpr auto DISPLAY_DC = GPIO_NUM_7; -constexpr auto DISPLAY_WR = GPIO_NUM_8; -constexpr auto DISPLAY_RD = GPIO_NUM_9; -constexpr auto DISPLAY_RST = GPIO_NUM_5; -constexpr auto DISPLAY_BL = GPIO_NUM_38; -constexpr auto DISPLAY_I80_D0 = GPIO_NUM_39; -constexpr auto DISPLAY_I80_D1 = GPIO_NUM_40; -constexpr auto DISPLAY_I80_D2 = GPIO_NUM_41; -constexpr auto DISPLAY_I80_D3 = GPIO_NUM_42; -constexpr auto DISPLAY_I80_D4 = GPIO_NUM_45; -constexpr auto DISPLAY_I80_D5 = GPIO_NUM_46; -constexpr auto DISPLAY_I80_D6 = GPIO_NUM_47; -constexpr auto DISPLAY_I80_D7 = GPIO_NUM_48; -constexpr auto DISPLAY_HORIZONTAL_RESOLUTION = 170; -constexpr auto DISPLAY_VERTICAL_RESOLUTION = 320; - -// Factory function for registration -std::shared_ptr createDisplay(); diff --git a/Devices/lilygo-tdisplay-s3/Source/devices/Power.cpp b/Devices/lilygo-tdisplay-s3/Source/devices/Power.cpp deleted file mode 100644 index febacad19..000000000 --- a/Devices/lilygo-tdisplay-s3/Source/devices/Power.cpp +++ /dev/null @@ -1,12 +0,0 @@ -#include "Power.h" - -#include -#include - -std::shared_ptr createPower() { - ChargeFromAdcVoltage::Configuration configuration; - // 2.0 ratio, but +.11 added as display voltage sag compensation. - configuration.adcMultiplier = 2.11; - - return std::make_shared(configuration); -} \ No newline at end of file diff --git a/Devices/lilygo-tdisplay-s3/Source/devices/Power.h b/Devices/lilygo-tdisplay-s3/Source/devices/Power.h deleted file mode 100644 index f2e392463..000000000 --- a/Devices/lilygo-tdisplay-s3/Source/devices/Power.h +++ /dev/null @@ -1,9 +0,0 @@ -#pragma once - -#include -#include -#include - -constexpr auto TDISPLAY_S3_POWERON_GPIO = GPIO_NUM_15; - -std::shared_ptr createPower(); diff --git a/Devices/lilygo-tdisplay-s3/Source/module.cpp b/Devices/lilygo-tdisplay-s3/Source/module.cpp deleted file mode 100644 index 06b55e106..000000000 --- a/Devices/lilygo-tdisplay-s3/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 lilygo_tdisplay_s3_module = { - .name = "lilygo-tdisplay-s3", - .start = start, - .stop = stop, - .symbols = nullptr, - .internal = nullptr -}; - -} diff --git a/Devices/lilygo-tdisplay-s3/device.properties b/Devices/lilygo-tdisplay-s3/device.properties index 759018c91..009ff559f 100644 --- a/Devices/lilygo-tdisplay-s3/device.properties +++ b/Devices/lilygo-tdisplay-s3/device.properties @@ -13,6 +13,8 @@ hardware.tinyUsb=true hardware.esptoolFlashFreq=120M hardware.bluetooth=true +dependencies.useDeprecatedHal=false + storage.userDataLocation=Internal display.size=1.9" diff --git a/Devices/lilygo-tdisplay-s3/devicetree.yaml b/Devices/lilygo-tdisplay-s3/devicetree.yaml index 52f68f0de..dcaa4453a 100644 --- a/Devices/lilygo-tdisplay-s3/devicetree.yaml +++ b/Devices/lilygo-tdisplay-s3/devicetree.yaml @@ -1,3 +1,5 @@ dependencies: - Platforms/platform-esp32 + - Drivers/st7789-i8080-module + - Drivers/button-control-module dts: lilygo,tdisplay-s3.dts diff --git a/Devices/lilygo-tdisplay-s3/lilygo,tdisplay-s3.dts b/Devices/lilygo-tdisplay-s3/lilygo,tdisplay-s3.dts index aa4236b74..778c7f0a7 100644 --- a/Devices/lilygo-tdisplay-s3/lilygo,tdisplay-s3.dts +++ b/Devices/lilygo-tdisplay-s3/lilygo,tdisplay-s3.dts @@ -4,6 +4,13 @@ #include #include #include +#include +#include +#include +#include +#include +#include +#include / { compatible = "root"; @@ -23,4 +30,72 @@ compatible = "espressif,esp32-gpio"; gpio-count = <49>; }; + + // Board peripheral power rail (GPIO15). The kernel_init() module-start pass (source/module.cpp) + // asserts it before any devicetree device is constructed/started - see that file's start(). + adc0 { + compatible = "espressif,esp32-adc-oneshot"; + unit-id = ; + channels = ; + }; + + // Matches the deprecated HAL's old Power.cpp (EstimatedPower/ChargeFromAdcVoltage default + // config): ADC1 CH3, 2:1 divider with +0.11 display voltage sag compensation + // (adcMultiplier = 2.11, so multiplier = 2110). + battery-sense { + compatible = "battery-sense"; + io-channel = <&adc0 0>; + reference-voltage-mv = <3300>; + multiplier = <2110>; + }; + + display_backlight_pwm { + compatible = "espressif,esp32-pwm-ledc"; + pin = <&gpio0 38 GPIO_FLAG_NONE>; + period-ns = <33333>; + 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 7 GPIO_FLAG_NONE>; + pin-wr = <&gpio0 8 GPIO_FLAG_NONE>; + pin-rd = <&gpio0 9 GPIO_FLAG_NONE>; + pin-d0 = <&gpio0 39 GPIO_FLAG_NONE>; + pin-d1 = <&gpio0 40 GPIO_FLAG_NONE>; + pin-d2 = <&gpio0 41 GPIO_FLAG_NONE>; + pin-d3 = <&gpio0 42 GPIO_FLAG_NONE>; + pin-d4 = <&gpio0 45 GPIO_FLAG_NONE>; + pin-d5 = <&gpio0 46 GPIO_FLAG_NONE>; + pin-d6 = <&gpio0 47 GPIO_FLAG_NONE>; + pin-d7 = <&gpio0 48 GPIO_FLAG_NONE>; + // horizontal-resolution * (vertical-resolution / 10 partial buffer) * 2 bytes/pixel + max-transfer-bytes = <10880>; + cs-gpios = <&gpio0 6 GPIO_FLAG_NONE>; + + display@0 { + compatible = "sitronix,st7789-i8080"; + horizontal-resolution = <170>; + vertical-resolution = <320>; + gap-x = <35>; + pixel-clock-hz = <10000000>; + pin-reset = <&gpio0 5 GPIO_FLAG_NONE>; + backlight = <&display_backlight>; + }; + }; + + buttons { + compatible = "tactility,button-control"; + pin-primary = <&gpio0 0 GPIO_FLAG_NONE>; + pin-secondary = <&gpio0 14 GPIO_FLAG_NONE>; + }; }; diff --git a/Devices/lilygo-tdisplay-s3/source/module.cpp b/Devices/lilygo-tdisplay-s3/source/module.cpp new file mode 100644 index 000000000..615dd1878 --- /dev/null +++ b/Devices/lilygo-tdisplay-s3/source/module.cpp @@ -0,0 +1,45 @@ +#include + +#include + +// Board peripheral power-enable pin (display, backlight, etc). Must be asserted before the +// devicetree devices below start - kernel_init() starts all dts_modules[] (this one included) +// before constructing any dts_devices[], so doing it here in start() runs early enough. +constexpr auto POWER_ON_PIN = GPIO_NUM_15; + +extern "C" { + +static error_t start() { + gpio_config_t power_signal_config = { + .pin_bit_mask = BIT64(POWER_ON_PIN), + .mode = GPIO_MODE_OUTPUT, + .pull_up_en = GPIO_PULLUP_DISABLE, + .pull_down_en = GPIO_PULLDOWN_DISABLE, + .intr_type = GPIO_INTR_DISABLE, + }; + + if (gpio_config(&power_signal_config) != ESP_OK) { + return ERROR_RESOURCE; + } + + if (gpio_set_level(POWER_ON_PIN, 1) != ESP_OK) { + return ERROR_RESOURCE; + } + + return ERROR_NONE; +} + +static error_t stop() { + // Empty for now + return ERROR_NONE; +} + +struct Module lilygo_tdisplay_s3_module = { + .name = "lilygo-tdisplay-s3", + .start = start, + .stop = stop, + .symbols = nullptr, + .internal = nullptr +}; + +} diff --git a/Devices/lilygo-tdisplay/CMakeLists.txt b/Devices/lilygo-tdisplay/CMakeLists.txt index 4e7c077a2..a832a83a2 100644 --- a/Devices/lilygo-tdisplay/CMakeLists.txt +++ b/Devices/lilygo-tdisplay/CMakeLists.txt @@ -1,7 +1,6 @@ -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 ButtonControl esp_lvgl_port ST7789 PwmBacklight driver + REQUIRES TactilityKernel driver ) diff --git a/Devices/lilygo-tdisplay/Source/Configuration.cpp b/Devices/lilygo-tdisplay/Source/Configuration.cpp deleted file mode 100644 index fc238bd95..000000000 --- a/Devices/lilygo-tdisplay/Source/Configuration.cpp +++ /dev/null @@ -1,23 +0,0 @@ -#include "devices/Display.h" - -#include -#include -#include - -using namespace tt::hal; - -static bool initBoot() { - return driver::pwmbacklight::init(LCD_PIN_BACKLIGHT); -} - -static std::vector> createDevices() { - return { - createDisplay(), - ButtonControl::createTwoButtonControl(35, 0) - }; -} - -extern const Configuration hardwareConfiguration = { - .initBoot = initBoot, - .createDevices = createDevices -}; diff --git a/Devices/lilygo-tdisplay/Source/devices/Display.cpp b/Devices/lilygo-tdisplay/Source/devices/Display.cpp deleted file mode 100644 index 45069b35b..000000000 --- a/Devices/lilygo-tdisplay/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 = true - }; - - 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); -} \ No newline at end of file diff --git a/Devices/lilygo-tdisplay/Source/devices/Display.h b/Devices/lilygo-tdisplay/Source/devices/Display.h deleted file mode 100644 index a0b48794f..000000000 --- a/Devices/lilygo-tdisplay/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_5; -constexpr auto LCD_PIN_DC = GPIO_NUM_16; -constexpr auto LCD_PIN_RESET = GPIO_NUM_23; -constexpr auto LCD_PIN_BACKLIGHT = GPIO_NUM_4; -constexpr auto LCD_HORIZONTAL_RESOLUTION = 135; -constexpr auto LCD_VERTICAL_RESOLUTION = 240; -constexpr auto LCD_BUFFER_HEIGHT = LCD_VERTICAL_RESOLUTION / 6; -constexpr auto LCD_BUFFER_SIZE = LCD_HORIZONTAL_RESOLUTION * LCD_BUFFER_HEIGHT; - -std::shared_ptr createDisplay(); diff --git a/Devices/lilygo-tdisplay/device.properties b/Devices/lilygo-tdisplay/device.properties index 590086137..b9c6c7060 100644 --- a/Devices/lilygo-tdisplay/device.properties +++ b/Devices/lilygo-tdisplay/device.properties @@ -10,6 +10,8 @@ hardware.flashSize=16MB hardware.spiRam=false hardware.esptoolFlashFreq=80M +dependencies.useDeprecatedHal=false + storage.userDataLocation=Internal display.size=1.14" diff --git a/Devices/lilygo-tdisplay/devicetree.yaml b/Devices/lilygo-tdisplay/devicetree.yaml index 1c6b1fe9d..e77bc9f2a 100644 --- a/Devices/lilygo-tdisplay/devicetree.yaml +++ b/Devices/lilygo-tdisplay/devicetree.yaml @@ -1,3 +1,5 @@ dependencies: - Platforms/platform-esp32 + - Drivers/st7789-module + - Drivers/button-control-module dts: lilygo,tdisplay.dts diff --git a/Devices/lilygo-tdisplay/lilygo,tdisplay.dts b/Devices/lilygo-tdisplay/lilygo,tdisplay.dts index f940daeef..877efc869 100644 --- a/Devices/lilygo-tdisplay/lilygo,tdisplay.dts +++ b/Devices/lilygo-tdisplay/lilygo,tdisplay.dts @@ -3,9 +3,11 @@ #include #include #include -#include #include -#include +#include +#include +#include +#include / { compatible = "root"; @@ -21,15 +23,46 @@ gpio-count = <49>; }; + display_backlight_pwm { + compatible = "espressif,esp32-pwm-ledc"; + pin = <&gpio0 4 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 5 GPIO_FLAG_NONE>; pin-mosi = <&gpio0 19 GPIO_FLAG_NONE>; pin-sclk = <&gpio0 18 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 = <62500000>; + pin-dc = <&gpio0 16 GPIO_FLAG_NONE>; + pin-reset = <&gpio0 23 GPIO_FLAG_NONE>; + backlight = <&display_backlight>; }; }; + + buttons { + compatible = "tactility,button-control"; + pin-primary = <&gpio0 35 GPIO_FLAG_NONE>; + pin-secondary = <&gpio0 0 GPIO_FLAG_NONE>; + }; }; diff --git a/Devices/lilygo-tdisplay/Source/module.cpp b/Devices/lilygo-tdisplay/source/module.cpp similarity index 100% rename from Devices/lilygo-tdisplay/Source/module.cpp rename to Devices/lilygo-tdisplay/source/module.cpp diff --git a/Devices/lilygo-tdongle-s3/CMakeLists.txt b/Devices/lilygo-tdongle-s3/CMakeLists.txt index cebdb1756..a832a83a2 100644 --- a/Devices/lilygo-tdongle-s3/CMakeLists.txt +++ b/Devices/lilygo-tdongle-s3/CMakeLists.txt @@ -1,7 +1,6 @@ -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 ST7735 PwmBacklight driver + REQUIRES TactilityKernel driver ) diff --git a/Devices/lilygo-tdongle-s3/Source/Configuration.cpp b/Devices/lilygo-tdongle-s3/Source/Configuration.cpp deleted file mode 100644 index d7462601c..000000000 --- a/Devices/lilygo-tdongle-s3/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/lilygo-tdongle-s3/Source/Init.cpp b/Devices/lilygo-tdongle-s3/Source/Init.cpp deleted file mode 100644 index 57e9b18ef..000000000 --- a/Devices/lilygo-tdongle-s3/Source/Init.cpp +++ /dev/null @@ -1,16 +0,0 @@ -#include "PwmBacklight.h" - -#include -#include -#include - -constexpr auto* TAG = "T-Dongle S3"; - -bool initBoot() { - if (!driver::pwmbacklight::init(GPIO_NUM_38, 12000)) { - LOG_E(TAG, "Backlight init failed"); - return false; - } - - return true; -} diff --git a/Devices/lilygo-tdongle-s3/Source/devices/Display.cpp b/Devices/lilygo-tdongle-s3/Source/devices/Display.cpp deleted file mode 100644 index 328d0ee48..000000000 --- a/Devices/lilygo-tdongle-s3/Source/devices/Display.cpp +++ /dev/null @@ -1,36 +0,0 @@ -#include "Display.h" - -#include -#include - -#define LCD_SPI_HOST SPI2_HOST -#define LCD_PIN_CS GPIO_NUM_4 -#define LCD_PIN_DC GPIO_NUM_2 -#define LCD_PIN_RESET GPIO_NUM_1 -#define LCD_HORIZONTAL_RESOLUTION 80 -#define LCD_VERTICAL_RESOLUTION 160 -#define LCD_SPI_TRANSFER_HEIGHT (LCD_VERTICAL_RESOLUTION / 4) - -std::shared_ptr createDisplay() { - auto configuration = std::make_unique( - LCD_SPI_HOST, - LCD_PIN_CS, - LCD_PIN_DC, - LCD_PIN_RESET, - 80, - 160, - nullptr, - false, - false, - false, - true, - 0, - 26, - 1 - ); - - configuration->backlightDutyFunction = driver::pwmbacklight::setBacklightDuty; - - auto display = std::make_shared(std::move(configuration)); - return std::reinterpret_pointer_cast(display); -} diff --git a/Devices/lilygo-tdongle-s3/Source/devices/Display.h b/Devices/lilygo-tdongle-s3/Source/devices/Display.h deleted file mode 100644 index 7a9b967d1..000000000 --- a/Devices/lilygo-tdongle-s3/Source/devices/Display.h +++ /dev/null @@ -1,5 +0,0 @@ -#pragma once - -#include - -std::shared_ptr createDisplay(); diff --git a/Devices/lilygo-tdongle-s3/device.properties b/Devices/lilygo-tdongle-s3/device.properties index f92ed6ebe..4b674248d 100644 --- a/Devices/lilygo-tdongle-s3/device.properties +++ b/Devices/lilygo-tdongle-s3/device.properties @@ -12,6 +12,8 @@ hardware.tinyUsb=true hardware.esptoolFlashFreq=120M hardware.bluetooth=true +dependencies.useDeprecatedHal=false + storage.userDataLocation=SD display.size=0.96" diff --git a/Devices/lilygo-tdongle-s3/devicetree.yaml b/Devices/lilygo-tdongle-s3/devicetree.yaml index 3ba9ef3c7..8ebecd7c7 100644 --- a/Devices/lilygo-tdongle-s3/devicetree.yaml +++ b/Devices/lilygo-tdongle-s3/devicetree.yaml @@ -1,3 +1,4 @@ dependencies: - Platforms/platform-esp32 + - Drivers/st7735-module dts: lilygo,tdongle-s3.dts diff --git a/Devices/lilygo-tdongle-s3/lilygo,tdongle-s3.dts b/Devices/lilygo-tdongle-s3/lilygo,tdongle-s3.dts index 8cb047fa1..6cfc3455e 100644 --- a/Devices/lilygo-tdongle-s3/lilygo,tdongle-s3.dts +++ b/Devices/lilygo-tdongle-s3/lilygo,tdongle-s3.dts @@ -8,7 +8,9 @@ #include #include #include -#include +#include +#include +#include / { compatible = "root"; @@ -37,15 +39,41 @@ pin-scl = <&gpio0 43 GPIO_FLAG_NONE>; }; + display_backlight_pwm { + compatible = "espressif,esp32-pwm-ledc"; + pin = <&gpio0 38 GPIO_FLAG_NONE>; + period-ns = <83333>; + 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 4 GPIO_FLAG_NONE>; pin-mosi = <&gpio0 3 GPIO_FLAG_NONE>; pin-sclk = <&gpio0 5 GPIO_FLAG_NONE>; - - display { - compatible = "display-placeholder"; + + display@0 { + compatible = "sitronix,st7735"; + horizontal-resolution = <80>; + vertical-resolution = <160>; + gap-x = <26>; + gap-y = <1>; + invert-color; + bgr-order; + pixel-clock-hz = <27000000>; + pin-dc = <&gpio0 2 GPIO_FLAG_NONE>; + pin-reset = <&gpio0 1 GPIO_FLAG_NONE>; + backlight = <&display_backlight>; }; }; diff --git a/Devices/lilygo-tdongle-s3/Source/module.cpp b/Devices/lilygo-tdongle-s3/source/module.cpp similarity index 100% rename from Devices/lilygo-tdongle-s3/Source/module.cpp rename to Devices/lilygo-tdongle-s3/source/module.cpp diff --git a/Devices/lilygo-tlora-pager/CMakeLists.txt b/Devices/lilygo-tlora-pager/CMakeLists.txt index e03a44c20..4ec49859d 100644 --- a/Devices/lilygo-tlora-pager/CMakeLists.txt +++ b/Devices/lilygo-tlora-pager/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_lcd ST7796 BQ25896 BQ27220 TCA8418 DRV2605 PwmBacklight driver esp_adc platform-esp32 + INCLUDE_DIRS "source" + REQUIRES Tactility driver esp_adc platform-esp32 lilygo-module ) diff --git a/Devices/lilygo-tlora-pager/Source/Configuration.cpp b/Devices/lilygo-tlora-pager/Source/Configuration.cpp deleted file mode 100644 index 5309e1aeb..000000000 --- a/Devices/lilygo-tlora-pager/Source/Configuration.cpp +++ /dev/null @@ -1,38 +0,0 @@ -#include "devices/Display.h" -#include "devices/TpagerEncoder.h" -#include "devices/TpagerKeyboard.h" -#include "devices/TpagerPower.h" -#include - -#include -#include -#include - -bool tpagerInit(); - -using namespace tt::hal; - -static DeviceVector createDevices() { - auto* i2c = device_find_by_name("i2c0"); - auto bq27220 = std::make_shared(i2c); - auto power = std::make_shared(bq27220); - - auto tca8418 = std::make_shared(i2c); - auto keyboard = std::make_shared(tca8418); - - return std::vector> { - tca8418, - std::make_shared(i2c), - bq27220, - std::make_shared(i2c), - power, - createDisplay(), - keyboard, - std::make_shared() - }; -} - -extern const Configuration hardwareConfiguration = { - .initBoot = tpagerInit, - .createDevices = createDevices -}; diff --git a/Devices/lilygo-tlora-pager/Source/Init.cpp b/Devices/lilygo-tlora-pager/Source/Init.cpp deleted file mode 100644 index 96e4ecf98..000000000 --- a/Devices/lilygo-tlora-pager/Source/Init.cpp +++ /dev/null @@ -1,57 +0,0 @@ -#include -#include -#include -#include -#include - -#include - -#include -#include - -constexpr auto* TAG = "T-Lora Pager"; - -bool tpagerInit() { - LOG_I(TAG, LOG_MESSAGE_POWER_ON_START); - - /* 32 Khz and higher gives an issue where the screen starts dimming again above 80% brightness - * when moving the brightness slider rapidly from a lower setting to 100%. - * This is not a slider bug (data was debug-traced) */ - if (!driver::pwmbacklight::init(GPIO_NUM_42, 30000)) { - LOG_E(TAG, "Backlight init failed"); - return false; - } - - tt::kernel::subscribeSystemEvent(tt::kernel::SystemEvent::BootSplash, [](auto) { - tt::hal::findDevices([](auto device) { - if (device->getName() == "BQ27220") { - auto bq27220 = std::reinterpret_pointer_cast(device); - if (bq27220 != nullptr) { - bq27220->configureCapacity(1500, 1500); - return false; - } - } - - return true; - }); - - auto gps_service = tt::service::gps::findGpsService(); - if (gps_service != nullptr) { - std::vector gps_configurations; - gps_service->getGpsConfigurations(gps_configurations); - if (gps_configurations.empty()) { - if (gps_service->addGpsConfiguration(tt::hal::gps::GpsConfiguration { - .uartName = "uart0", - .baudRate = 38400, - .model = tt::hal::gps::GpsModel::UBLOX10 - })) { - LOG_I(TAG, "Configured internal GPS"); - } else { - LOG_E(TAG, "Failed to configure internal GPS"); - } - } - } - }); - - return true; -} diff --git a/Devices/lilygo-tlora-pager/Source/bindings/tlora_pager.h b/Devices/lilygo-tlora-pager/Source/bindings/tlora_pager.h deleted file mode 100644 index a126dd079..000000000 --- a/Devices/lilygo-tlora-pager/Source/bindings/tlora_pager.h +++ /dev/null @@ -1,15 +0,0 @@ -#pragma once - -#ifdef __cplusplus -extern "C" { -#endif - -#include -#include -#include - -DEFINE_DEVICETREE(tlora_pager, struct RootConfig) - -#ifdef __cplusplus -} -#endif diff --git a/Devices/lilygo-tlora-pager/Source/devices/Display.cpp b/Devices/lilygo-tlora-pager/Source/devices/Display.cpp deleted file mode 100644 index 84a42d186..000000000 --- a/Devices/lilygo-tlora-pager/Source/devices/Display.cpp +++ /dev/null @@ -1,33 +0,0 @@ -#include "Display.h" - -#include -#include - -#define TPAGER_LCD_SPI_HOST SPI2_HOST -#define TPAGER_LCD_PIN_CS GPIO_NUM_38 -#define TPAGER_LCD_PIN_DC GPIO_NUM_37 // RS -#define TPAGER_LCD_HORIZONTAL_RESOLUTION 222 -#define TPAGER_LCD_VERTICAL_RESOLUTION 480 -#define TPAGER_LCD_SPI_TRANSFER_HEIGHT (TPAGER_LCD_VERTICAL_RESOLUTION / 10) - -std::shared_ptr createDisplay() { - auto configuration = std::make_unique( - TPAGER_LCD_SPI_HOST, - TPAGER_LCD_PIN_CS, - TPAGER_LCD_PIN_DC, - 480, // w - 222, // h - nullptr, - true, //swapXY - true, //mirrorX - true, //mirrorY - true, //invertColor - 0, //gapX - 49 //gapY - ); - - configuration->backlightDutyFunction = driver::pwmbacklight::setBacklightDuty; - - auto display = std::make_shared(std::move(configuration)); - return std::reinterpret_pointer_cast(display); -} diff --git a/Devices/lilygo-tlora-pager/Source/devices/Display.h b/Devices/lilygo-tlora-pager/Source/devices/Display.h deleted file mode 100644 index 7a9b967d1..000000000 --- a/Devices/lilygo-tlora-pager/Source/devices/Display.h +++ /dev/null @@ -1,5 +0,0 @@ -#pragma once - -#include - -std::shared_ptr createDisplay(); diff --git a/Devices/lilygo-tlora-pager/Source/devices/TpagerEncoder.cpp b/Devices/lilygo-tlora-pager/Source/devices/TpagerEncoder.cpp deleted file mode 100644 index 571b2b132..000000000 --- a/Devices/lilygo-tlora-pager/Source/devices/TpagerEncoder.cpp +++ /dev/null @@ -1,213 +0,0 @@ -#include "TpagerEncoder.h" - -#include -#include - -constexpr auto* TAG = "TpagerEncoder"; - -constexpr auto ENCODER_A = GPIO_NUM_40; -constexpr auto ENCODER_B = GPIO_NUM_41; -constexpr auto ENCODER_ENTER = GPIO_NUM_7; - -void TpagerEncoder::readCallback(lv_indev_t* indev, lv_indev_data_t* data) { - TpagerEncoder* encoder = static_cast(lv_indev_get_user_data(indev)); - constexpr int enter_filter_threshold = 2; - static int enter_filter = 0; - constexpr int pulses_click = 4; - static int pulses_prev = 0; - - // Defaults - data->enc_diff = 0; - data->state = LV_INDEV_STATE_RELEASED; - - int pulses = encoder->getEncoderPulses(); - int pulse_diff = (pulses - pulses_prev); - if ((pulse_diff > pulses_click) || (pulse_diff < -pulses_click)) { - data->enc_diff = pulse_diff / pulses_click; - pulses_prev = pulses; - } - - bool enter = !gpio_get_level(ENCODER_ENTER); - if (enter && (enter_filter < enter_filter_threshold)) { - enter_filter++; - } - if (!enter && (enter_filter > 0)) { - enter_filter--; - } - - if (enter_filter == enter_filter_threshold) { - data->state = LV_INDEV_STATE_PRESSED; - } -} - -bool TpagerEncoder::initEncoder() { - assert(encPcntUnit == nullptr); - - constexpr int LOW_LIMIT = -127; - constexpr int HIGH_LIMIT = 126; - - // Accum. count makes it that over- and underflows are automatically compensated. - // Prerequisite: watchpoints at low and high limit - pcnt_unit_config_t unit_config = { - .low_limit = LOW_LIMIT, - .high_limit = HIGH_LIMIT, - .intr_priority = 0, - .flags = { - .accum_count = 1 - }, - }; - - if (pcnt_new_unit(&unit_config, &encPcntUnit) != ESP_OK) { - LOG_E(TAG, "Pulsecounter initialization failed"); - return false; - } - - pcnt_glitch_filter_config_t filter_config = { - .max_glitch_ns = 1000, - }; - - if (pcnt_unit_set_glitch_filter(encPcntUnit, &filter_config) != ESP_OK) { - LOG_E(TAG, "Pulsecounter glitch filter config failed"); - pcnt_del_unit(encPcntUnit); - encPcntUnit = nullptr; - return false; - } - - pcnt_chan_config_t chan_1_config = { - .edge_gpio_num = ENCODER_B, - .level_gpio_num = ENCODER_A, - .flags { - .invert_edge_input = 0, - .invert_level_input = 0, - .virt_edge_io_level = 0, - .virt_level_io_level = 0, - .io_loop_back = 0 - } - }; - - pcnt_chan_config_t chan_2_config = { - .edge_gpio_num = ENCODER_A, - .level_gpio_num = ENCODER_B, - .flags { - .invert_edge_input = 0, - .invert_level_input = 0, - .virt_edge_io_level = 0, - .virt_level_io_level = 0, - .io_loop_back = 0 - } - }; - - pcnt_channel_handle_t pcnt_chan_1 = nullptr; - pcnt_channel_handle_t pcnt_chan_2 = nullptr; - - if ((pcnt_new_channel(encPcntUnit, &chan_1_config, &pcnt_chan_1) != ESP_OK) || - (pcnt_new_channel(encPcntUnit, &chan_2_config, &pcnt_chan_2) != ESP_OK)) { - LOG_E(TAG, "Pulsecounter channel config failed"); - pcnt_del_unit(encPcntUnit); - encPcntUnit = nullptr; - return false; - } - - // Second argument is rising edge, third argument is falling edge - if ((pcnt_channel_set_edge_action(pcnt_chan_1, PCNT_CHANNEL_EDGE_ACTION_DECREASE, PCNT_CHANNEL_EDGE_ACTION_INCREASE) != ESP_OK) || - (pcnt_channel_set_edge_action(pcnt_chan_2, PCNT_CHANNEL_EDGE_ACTION_INCREASE, PCNT_CHANNEL_EDGE_ACTION_DECREASE) != ESP_OK)) { - LOG_E(TAG, "Pulsecounter edge action config failed"); - pcnt_del_unit(encPcntUnit); - encPcntUnit = nullptr; - return false; - } - - // Second argument is low level, third argument is high level - if ((pcnt_channel_set_level_action(pcnt_chan_1, PCNT_CHANNEL_LEVEL_ACTION_KEEP, PCNT_CHANNEL_LEVEL_ACTION_INVERSE) != ESP_OK) || - (pcnt_channel_set_level_action(pcnt_chan_2, PCNT_CHANNEL_LEVEL_ACTION_KEEP, PCNT_CHANNEL_LEVEL_ACTION_INVERSE) != ESP_OK)) { - LOG_E(TAG, "Pulsecounter level action config failed"); - pcnt_del_unit(encPcntUnit); - encPcntUnit = nullptr; - return false; - } - - if ((pcnt_unit_add_watch_point(encPcntUnit, LOW_LIMIT) != ESP_OK) || - (pcnt_unit_add_watch_point(encPcntUnit, HIGH_LIMIT) != ESP_OK)) { - LOG_E(TAG, "Pulsecounter watch point config failed"); - pcnt_del_unit(encPcntUnit); - encPcntUnit = nullptr; - return false; - } - - if (pcnt_unit_enable(encPcntUnit) != ESP_OK) { - LOG_E(TAG, "Pulsecounter could not be enabled"); - pcnt_del_unit(encPcntUnit); - encPcntUnit = nullptr; - return false; - } - - if (pcnt_unit_clear_count(encPcntUnit) != ESP_OK) { - LOG_E(TAG, "Pulsecounter could not be cleared"); - pcnt_del_unit(encPcntUnit); - encPcntUnit = nullptr; - return false; - } - - if (pcnt_unit_start(encPcntUnit) != ESP_OK) { - LOG_E(TAG, "Pulsecounter could not be started"); - pcnt_del_unit(encPcntUnit); - encPcntUnit = nullptr; - return false; - } - - return true; -} - -int TpagerEncoder::getEncoderPulses() const { - int pulses = 0; - pcnt_unit_get_count(encPcntUnit, &pulses); - return pulses; -} - -bool TpagerEncoder::deinitEncoder() { - assert(encPcntUnit != nullptr); - - if (pcnt_unit_stop(encPcntUnit) != ESP_OK) { - LOG_W(TAG, "Failed to stop encoder"); - } - - if (pcnt_del_unit(encPcntUnit) != ESP_OK) { - LOG_W(TAG, "Failed to delete encoder"); - encPcntUnit = nullptr; - return false; - } - - LOG_I(TAG, "Deinitialized"); - - return true; -} - - -bool TpagerEncoder::startLvgl(lv_display_t* display) { - if (encPcntUnit == nullptr && !initEncoder()) { - return false; - } - - gpio_input_enable(ENCODER_ENTER); - - encHandle = lv_indev_create(); - - lv_indev_set_type(encHandle, LV_INDEV_TYPE_ENCODER); - lv_indev_set_read_cb(encHandle, &readCallback); - lv_indev_set_display(encHandle, display); - lv_indev_set_user_data(encHandle, this); - - return true; -} - -bool TpagerEncoder::stopLvgl() { - lv_indev_delete(encHandle); - encHandle = nullptr; - - if (encPcntUnit != nullptr && !deinitEncoder()) { - // We're not returning false as LVGL as effectively deinitialized - LOG_W(TAG, "Deinitialization failed"); - } - - return true; -} diff --git a/Devices/lilygo-tlora-pager/Source/devices/TpagerEncoder.h b/Devices/lilygo-tlora-pager/Source/devices/TpagerEncoder.h deleted file mode 100644 index 859484704..000000000 --- a/Devices/lilygo-tlora-pager/Source/devices/TpagerEncoder.h +++ /dev/null @@ -1,30 +0,0 @@ -#pragma once - -#include -#include - -class TpagerEncoder final : public tt::hal::encoder::EncoderDevice { - - lv_indev_t* encHandle = nullptr; - pcnt_unit_handle_t encPcntUnit = nullptr; - - bool initEncoder(); - bool deinitEncoder(); - - static void readCallback(lv_indev_t* indev, lv_indev_data_t* data); - -public: - - TpagerEncoder() {} - ~TpagerEncoder() override {} - - std::string getName() const override { return "T-Lora Pager Encoder"; } - std::string getDescription() const override { return "The encoder wheel next to the display"; } - - bool startLvgl(lv_display_t* display) override; - bool stopLvgl() override; - - int getEncoderPulses() const; - - lv_indev_t* getLvglIndev() override { return encHandle; } -}; diff --git a/Devices/lilygo-tlora-pager/Source/devices/TpagerKeyboard.cpp b/Devices/lilygo-tlora-pager/Source/devices/TpagerKeyboard.cpp deleted file mode 100644 index 5bcbbc7c9..000000000 --- a/Devices/lilygo-tlora-pager/Source/devices/TpagerKeyboard.cpp +++ /dev/null @@ -1,219 +0,0 @@ -#include "TpagerKeyboard.h" - -#include -#include -#include - -constexpr auto* TAG = "TpagerKeyboard"; - -constexpr auto BACKLIGHT = GPIO_NUM_46; - -constexpr auto KB_ROWS = 4; -constexpr auto KB_COLS = 10; - -// Lowercase Keymap -static constexpr char keymap_lc[KB_ROWS][KB_COLS] = { - {'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p'}, - {'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', LV_KEY_ENTER}, - {'\0', 'z', 'x', 'c', 'v', 'b', 'n', 'm', '\0', LV_KEY_BACKSPACE}, - {' ', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0'} -}; - -// Uppercase Keymap -static constexpr char keymap_uc[KB_ROWS][KB_COLS] = { - {'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P'}, - {'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', LV_KEY_ENTER}, - {'\0', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', '\0', LV_KEY_BACKSPACE}, - {' ', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0'} -}; - -// Symbol Keymap -static constexpr char keymap_sy[KB_ROWS][KB_COLS] = { - {'1', '2', '3', '4', '5', '6', '7', '8', '9', '0'}, - {'.', '/', '+', '-', '=', ':', '\'', '"', '@', '\t'}, - {'\0', '_', '$', ';', '?', '!', ',', '.', '\0', LV_KEY_BACKSPACE}, - {' ', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0'} -}; - -void TpagerKeyboard::readCallback(lv_indev_t* indev, lv_indev_data_t* data) { - auto keyboard = static_cast(lv_indev_get_user_data(indev)); - char keypress = 0; - - if (xQueueReceive(keyboard->queue, &keypress, 0) == pdPASS) { - data->key = keypress; - data->state = LV_INDEV_STATE_PRESSED; - } else { - data->key = 0; - data->state = LV_INDEV_STATE_RELEASED; - } -} - -void TpagerKeyboard::processKeyboard() { - static bool shift_pressed = false; - static bool sym_pressed = false; - static bool cap_toggle = false; - static bool cap_toggle_armed = true; - bool anykey_pressed = false; - - if (keypad->update()) { - anykey_pressed = (keypad->pressed_key_count > 0); - for (int i = 0; i < keypad->pressed_key_count; i++) { - auto row = keypad->pressed_list[i].row; - auto col = keypad->pressed_list[i].col; - auto hold = keypad->pressed_list[i].hold_time; - - if ((row == 2) && (col == 0)) { - sym_pressed = true; - } - if ((row == 2) && (col == 8)) { - shift_pressed = true; - } - } - - if ((sym_pressed && shift_pressed) && cap_toggle_armed) { - cap_toggle = !cap_toggle; - cap_toggle_armed = false; - } - - for (int i = 0; i < keypad->pressed_key_count; i++) { - auto row = keypad->pressed_list[i].row; - auto col = keypad->pressed_list[i].col; - auto hold = keypad->pressed_list[i].hold_time; - char chr = '\0'; - if (sym_pressed) { - chr = keymap_sy[row][col]; - } else if (shift_pressed || cap_toggle) { - chr = keymap_uc[row][col]; - } else { - chr = keymap_lc[row][col]; - } - - if (chr != '\0') xQueueSend(queue, &chr, 50 / portTICK_PERIOD_MS); - } - - for (int i = 0; i < keypad->released_key_count; i++) { - auto row = keypad->released_list[i].row; - auto col = keypad->released_list[i].col; - - if ((row == 2) && (col == 0)) { - sym_pressed = false; - } - if ((row == 2) && (col == 8)) { - shift_pressed = false; - } - } - - if ((!sym_pressed && !shift_pressed) && !cap_toggle_armed) { - cap_toggle_armed = true; - } - - if (anykey_pressed) { - makeBacklightImpulse(); - } - } -} - -bool TpagerKeyboard::startLvgl(lv_display_t* display) { - backlightOkay = initBacklight(BACKLIGHT, 30000, LEDC_TIMER_0, LEDC_CHANNEL_1); - keypad->init(KB_ROWS, KB_COLS); - - assert(inputTimer == nullptr); - inputTimer = std::make_unique(tt::Timer::Type::Periodic, tt::kernel::millisToTicks(20), [this] { - processKeyboard(); - }); - - assert(backlightImpulseTimer == nullptr); - backlightImpulseTimer = std::make_unique(tt::Timer::Type::Periodic, tt::kernel::millisToTicks(50), [this] { - processBacklightImpulse(); - }); - - 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); - - inputTimer->start(); - backlightImpulseTimer->start(); - - return true; -} - -bool TpagerKeyboard::stopLvgl() { - assert(inputTimer); - inputTimer->stop(); - inputTimer = nullptr; - - assert(backlightImpulseTimer); - backlightImpulseTimer->stop(); - backlightImpulseTimer = nullptr; - - lv_indev_delete(kbHandle); - kbHandle = nullptr; - return true; -} - -bool TpagerKeyboard::isAttached() const { - return i2c_controller_has_device_at_address(keypad->getController(), keypad->getAddress(), 100) == ERROR_NONE; -} - -bool TpagerKeyboard::initBacklight(gpio_num_t pin, uint32_t frequencyHz, ledc_timer_t timer, ledc_channel_t channel) { - backlightPin = pin; - backlightTimer = timer; - backlightChannel = channel; - - ledc_timer_config_t ledc_timer = { - .speed_mode = LEDC_LOW_SPEED_MODE, - .duty_resolution = LEDC_TIMER_8_BIT, - .timer_num = backlightTimer, - .freq_hz = frequencyHz, - .clk_cfg = LEDC_AUTO_CLK, - .deconfigure = false - }; - - if (ledc_timer_config(&ledc_timer) != ESP_OK) { - LOG_E(TAG, "Backlight timer config failed"); - return false; - } - - ledc_channel_config_t ledc_channel = { - .gpio_num = backlightPin, - .speed_mode = LEDC_LOW_SPEED_MODE, - .channel = backlightChannel, - .intr_type = LEDC_INTR_DISABLE, - .timer_sel = backlightTimer, - .duty = 0, - .hpoint = 0, - .sleep_mode = LEDC_SLEEP_MODE_NO_ALIVE_NO_PD, - .flags = { - .output_invert = 0 - } - }; - - if (ledc_channel_config(&ledc_channel) != ESP_OK) { - LOG_E(TAG, "Backlight channel config failed"); - } - - return true; -} - -bool TpagerKeyboard::setBacklightDuty(uint8_t duty) { - if (!backlightOkay) { - LOG_E(TAG, "Backlight not ready"); - return false; - } - return (ledc_set_duty(LEDC_LOW_SPEED_MODE, backlightChannel, duty) == ESP_OK) && - (ledc_update_duty(LEDC_LOW_SPEED_MODE, backlightChannel) == ESP_OK); -} - -void TpagerKeyboard::makeBacklightImpulse() { - backlightImpulseDuty = 255; - setBacklightDuty(backlightImpulseDuty); -} - -void TpagerKeyboard::processBacklightImpulse() { - if (backlightImpulseDuty > 64) { - backlightImpulseDuty--; - setBacklightDuty(backlightImpulseDuty); - } -} diff --git a/Devices/lilygo-tlora-pager/Source/devices/TpagerKeyboard.h b/Devices/lilygo-tlora-pager/Source/devices/TpagerKeyboard.h deleted file mode 100644 index f8a40e5ef..000000000 --- a/Devices/lilygo-tlora-pager/Source/devices/TpagerKeyboard.h +++ /dev/null @@ -1,52 +0,0 @@ -#pragma once - -#include -#include - -#include -#include -#include -#include - -class TpagerKeyboard final : public tt::hal::keyboard::KeyboardDevice { - - lv_indev_t* kbHandle = nullptr; - gpio_num_t backlightPin = GPIO_NUM_NC; - ledc_timer_t backlightTimer; - ledc_channel_t backlightChannel; - bool backlightOkay = false; - int backlightImpulseDuty = 0; - QueueHandle_t queue = nullptr; - - std::shared_ptr keypad; - std::unique_ptr inputTimer; - std::unique_ptr backlightImpulseTimer; - - bool initBacklight(gpio_num_t pin, uint32_t frequencyHz, ledc_timer_t timer, ledc_channel_t channel); - void processKeyboard(); - void processBacklightImpulse(); - - static void readCallback(lv_indev_t* indev, lv_indev_data_t* data); - -public: - - explicit TpagerKeyboard(const std::shared_ptr& tca) : keypad(tca) { - queue = xQueueCreate(20, sizeof(char)); - } - - ~TpagerKeyboard() override { - vQueueDelete(queue); - } - - std::string getName() const override { return "T-Lora Pager Keyboard"; } - std::string getDescription() const override { return "T-Lora Pager I2C keyboard with encoder"; } - - bool startLvgl(lv_display_t* display) override; - bool stopLvgl() override; - - bool isAttached() const override; - lv_indev_t* getLvglIndev() override { return kbHandle; } - - bool setBacklightDuty(uint8_t duty); - void makeBacklightImpulse(); -}; diff --git a/Devices/lilygo-tlora-pager/Source/devices/TpagerPower.cpp b/Devices/lilygo-tlora-pager/Source/devices/TpagerPower.cpp deleted file mode 100644 index e82e1c697..000000000 --- a/Devices/lilygo-tlora-pager/Source/devices/TpagerPower.cpp +++ /dev/null @@ -1,77 +0,0 @@ -#include "TpagerPower.h" - -#include -#include - -constexpr auto* TAG = "TpagerPower"; - -TpagerPower::~TpagerPower() {} - -bool TpagerPower::supportsMetric(MetricType type) const { - switch (type) { - using enum MetricType; - case IsCharging: - case Current: - case BatteryVoltage: - case ChargeLevel: - return true; - default: - return false; - } - - return false; // Safety guard for when new enum values are introduced -} - -bool TpagerPower::getMetric(MetricType type, MetricData& data) { - uint16_t u16 = 0; - int16_t s16 = 0; - switch (type) { - using enum MetricType; - case IsCharging: - Bq27220::BatteryStatus status; - if (gauge->getBatteryStatus(status)) { - data.valueAsBool = !status.reg.DSG; - return true; - } - return false; - case Current: - if (gauge->getCurrent(s16)) { - data.valueAsInt32 = s16; - return true; - } else { - return false; - } - case BatteryVoltage: - if (gauge->getVoltage(u16)) { - data.valueAsUint32 = u16; - return true; - } else { - return false; - } - case ChargeLevel: - if (gauge->getStateOfCharge(u16)) { - data.valueAsUint8 = u16; - return true; - } else { - return false; - } - default: - return false; - } -} - -void TpagerPower::powerOff() { - auto device = tt::hal::findDevice([](auto device) { - return device->getName() == "BQ25896"; - }); - - if (device == nullptr) { - LOG_E(TAG, "BQ25896 not found"); - return; - } - - auto bq25896 = std::reinterpret_pointer_cast(device); - if (bq25896 != nullptr) { - bq25896->powerOff(); - } -} diff --git a/Devices/lilygo-tlora-pager/Source/devices/TpagerPower.h b/Devices/lilygo-tlora-pager/Source/devices/TpagerPower.h deleted file mode 100644 index 345ce00cf..000000000 --- a/Devices/lilygo-tlora-pager/Source/devices/TpagerPower.h +++ /dev/null @@ -1,25 +0,0 @@ -#pragma once - -#include "Tactility/hal/power/PowerDevice.h" -#include -#include - -using tt::hal::power::PowerDevice; - -class TpagerPower : public PowerDevice { - std::shared_ptr gauge; - -public: - - TpagerPower(const std::shared_ptr& bq) : gauge(bq) {} - ~TpagerPower(); - - std::string getName() const final { return "T-LoRa Pager Power measument"; } - std::string getDescription() const final { return "Power measurement interface via I2C fuel gauge"; } - - bool supportsMetric(MetricType type) const override; - bool getMetric(MetricType type, MetricData& data) override; - - bool supportsPowerOff() const override { return true; } - void powerOff() override; -}; diff --git a/Devices/lilygo-tlora-pager/Source/drivers/TloraPager.cpp b/Devices/lilygo-tlora-pager/Source/drivers/TloraPager.cpp deleted file mode 100644 index 6a1f07f08..000000000 --- a/Devices/lilygo-tlora-pager/Source/drivers/TloraPager.cpp +++ /dev/null @@ -1,30 +0,0 @@ -#include "TloraPager.h" - -#include - -#include - -extern "C" { - -extern struct Module lilygo_tlora_pager_module; - -static int start(Device* device) { - return 0; -} - -static int stop(Device* device) { - return 0; -} - -Driver tlora_pager_driver = { - .name = "T-Lora Pager", - .compatible = (const char*[]) { "lilygo,tlora-pager", nullptr }, - .start_device = start, - .stop_device = stop, - .api = nullptr, - .device_type = nullptr, - .owner = &lilygo_tlora_pager_module, - .internal = nullptr -}; - -} diff --git a/Devices/lilygo-tlora-pager/Source/drivers/TloraPager.h b/Devices/lilygo-tlora-pager/Source/drivers/TloraPager.h deleted file mode 100644 index 7f30b2b43..000000000 --- a/Devices/lilygo-tlora-pager/Source/drivers/TloraPager.h +++ /dev/null @@ -1,11 +0,0 @@ -#pragma once - -#ifdef __cplusplus -extern "C" { -#endif - -#include - -#ifdef __cplusplus -} -#endif diff --git a/Devices/lilygo-tlora-pager/devicetree.yaml b/Devices/lilygo-tlora-pager/devicetree.yaml index 5d71c58eb..82ada517d 100644 --- a/Devices/lilygo-tlora-pager/devicetree.yaml +++ b/Devices/lilygo-tlora-pager/devicetree.yaml @@ -1,4 +1,9 @@ dependencies: - Platforms/platform-esp32 -bindings: ./ + - Drivers/st7796-module + - Drivers/bq27220-module + - Drivers/tca8418-module + - Drivers/bq25896-module + - Drivers/drv2605-module + - Drivers/lilygo-module dts: lilygo,tlora-pager.dts diff --git a/Devices/lilygo-tlora-pager/lilygo,tlora-pager.dts b/Devices/lilygo-tlora-pager/lilygo,tlora-pager.dts index af9c44b3b..f3343fb59 100644 --- a/Devices/lilygo-tlora-pager/lilygo,tlora-pager.dts +++ b/Devices/lilygo-tlora-pager/lilygo,tlora-pager.dts @@ -1,6 +1,6 @@ /dts-v1/; -#include +#include #include #include #include @@ -9,11 +9,18 @@ #include #include #include -#include +#include +#include +#include +#include +#include +#include +#include +#include -// Reference: https://wiki.lilygo.cc/get_started/en/LoRa_GPS/T-LoraPager/T-LoraPager.html#Pin-Overview +// Reference: https://wiki.lilygo.cc/get_started/en/LoRa_GPS/T-LoraPager/T-LoraPager.html / { - compatible = "lilygo,tlora-pager"; + compatible = "root"; model = "LilyGO T-Lora Pager"; wifi0 { @@ -37,26 +44,124 @@ clock-frequency = <100000>; pin-sda = <&gpio0 3 GPIO_FLAG_NONE>; pin-scl = <&gpio0 2 GPIO_FLAG_NONE>; + + bq27220 { + compatible = "ti,bq27220"; + reg = <0x55>; + }; + + // ALT (shift) is row 2 col 8; SYM (symbol layer) is row 2 col 0. + keyboard { + compatible = "ti,tca8418"; + reg = <0x34>; + rows = <4>; + columns = <10>; + keymap-lc = [ + 113 119 101 114 116 121 117 105 111 112 // q w e r t y u i o p + 97 115 100 102 103 104 106 107 108 10 // a s d f g h j k l ENTER + 0 122 120 99 118 98 110 109 0 8 // z x c v b n m BACKSPACE + 32 0 0 0 0 0 0 0 0 0 // SPC + ]; + keymap-uc = [ + 81 87 69 82 84 89 85 73 79 80 // Q W E R T Y U I O P + 65 83 68 70 71 72 74 75 76 10 // A S D F G H J K L ENTER + 0 90 88 67 86 66 78 77 0 8 // Z X C V B N M BACKSPACE + 32 0 0 0 0 0 0 0 0 0 // SPC + ]; + keymap-sy = [ + 49 50 51 52 53 54 55 56 57 48 // 1 2 3 4 5 6 7 8 9 0 + 46 47 43 45 61 58 39 34 64 9 // . / + - = : ' " @ TAB + 0 95 36 59 63 33 44 46 0 8 // _ $ ; ? ! , . BACKSPACE + 32 0 0 0 0 0 0 0 0 0 // SPC + ]; + shift-row = <2>; + shift-col = <8>; + sym-row = <2>; + sym-col = <0>; + }; + + bq25896 { + compatible = "ti,bq25896"; + reg = <0x6B>; + }; + + // Plays a 3x strong-click "buzz" once at boot to confirm the motor works (matches the + // deprecated HAL's Drv2605 default autoPlayStartupBuzz=true). + drv2605 { + compatible = "ti,drv2605"; + reg = <0x5A>; + }; + }; + + // Keyboard backlight (LED_PWM), GPIO46. The keypress-triggered flash/decay effect from the + // pre-migration driver isn't reproduced here - kernel drivers don't have a home for that kind + // of UI policy (same reasoning as the encoder/touch drivers elsewhere). Off by default; app + // code can drive brightness via the generic pwm-backlight API if desired. + keyboard_backlight_pwm { + compatible = "espressif,esp32-pwm-ledc"; + pin = <&gpio0 46 GPIO_FLAG_NONE>; + period-ns = <33333>; + ledc-timer = <1>; + ledc-channel = <1>; + }; + + keyboard_backlight { + compatible = "pwm-backlight"; + status = "disabled"; + pwm = <&keyboard_backlight_pwm>; + }; + + display_backlight_pwm { + compatible = "espressif,esp32-pwm-ledc"; + pin = <&gpio0 42 GPIO_FLAG_NONE>; + // 32 KHz and higher causes the screen to start dimming again above 80% brightness + // when moving the brightness slider rapidly from a lower setting to 100% (debug-traced, not a slider bug). + period-ns = <33333>; + 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 36 GPIO_FLAG_NONE>, // Display - <&gpio0 21 GPIO_FLAG_NONE>; // SD card + cs-gpios = <&gpio0 21 GPIO_FLAG_NONE>, // SD + <&gpio0 38 GPIO_FLAG_NONE>, // Display + <&gpio0 36 GPIO_FLAG_NONE>, // Lora + <&gpio0 39 GPIO_FLAG_NONE>; // NFC pin-mosi = <&gpio0 34 GPIO_FLAG_NONE>; pin-miso = <&gpio0 33 GPIO_FLAG_NONE>; pin-sclk = <&gpio0 35 GPIO_FLAG_NONE>; - - display@0 { - compatible = "display-placeholder"; - }; - - sdcard@1 { + miso-pull-up; + + sdcard@0 { compatible = "espressif,esp32-sdspi"; - status = "disabled"; // Must be started after display frequency-khz = <20000>; }; + + display@1 { + compatible = "sitronix,st7796"; + horizontal-resolution = <480>; + vertical-resolution = <222>; + swap-xy; + mirror-x; + mirror-y; + invert-color; + bgr-order; + gap-x = <0>; + gap-y = <49>; + pixel-clock-hz = <80000000>; + transaction-queue-depth = <2>; + pin-dc = <&gpio0 37 GPIO_FLAG_NONE>; + backlight = <&display_backlight>; + }; }; // ES8311 @@ -84,4 +189,12 @@ pin-tx = <&gpio0 43 GPIO_FLAG_NONE>; pin-rx = <&gpio0 44 GPIO_FLAG_NONE>; }; + + // Encoder wheel next to the display. + encoder { + compatible = "lilygo,tpager-encoder"; + pin-a = <&gpio0 40 GPIO_FLAG_NONE>; + pin-b = <&gpio0 41 GPIO_FLAG_NONE>; + pin-enter = <&gpio0 7 GPIO_FLAG_NONE>; + }; }; diff --git a/Devices/lilygo-tlora-pager/lilygo,tlora-pager.yaml b/Devices/lilygo-tlora-pager/lilygo,tlora-pager.yaml deleted file mode 100644 index 9e5f05085..000000000 --- a/Devices/lilygo-tlora-pager/lilygo,tlora-pager.yaml +++ /dev/null @@ -1,5 +0,0 @@ -description: LilyGO T-Lora Pager - -include: ["root.yaml"] - -compatible: "lilygo,tlora-pager" diff --git a/Devices/lilygo-tlora-pager/source/module.cpp b/Devices/lilygo-tlora-pager/source/module.cpp new file mode 100644 index 000000000..c8f05431b --- /dev/null +++ b/Devices/lilygo-tlora-pager/source/module.cpp @@ -0,0 +1,72 @@ +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +constexpr auto* TAG = "T-Lora Pager"; + +// Legacy placeholder (required until legacy HAL is cleaned up everywhere). All board +// peripherals are kernel drivers now (see the .dts): display (st7796), battery (bq27220), +// charger power-off (bq25896), haptics (drv2605), keyboard (tca8418) and the encoder wheel +// (lilygo,tpager-encoder, bound to LVGL below). +extern const tt::hal::Configuration hardwareConfiguration = {}; + +extern "C" { + +static void subscribe_events() { + LOG_I(TAG, LOG_MESSAGE_POWER_ON_START); + + tt::kernel::subscribeSystemEvent(tt::kernel::SystemEvent::BootSplash, [](tt::kernel::SystemEvent) { + // The kernel tpager_encoder device is already started by kernel_init(); this just + // registers it as an LVGL input device, which requires LVGL to be up first. + lvgl_lock(); + tpager_encoder::init(); + lvgl_unlock(); + + auto gps_service = tt::service::gps::findGpsService(); + if (gps_service != nullptr) { + std::vector gps_configurations; + gps_service->getGpsConfigurations(gps_configurations); + if (gps_configurations.empty()) { + if (gps_service->addGpsConfiguration(tt::hal::gps::GpsConfiguration { + .uartName = "uart0", + .baudRate = 38400, + .model = tt::hal::gps::GpsModel::UBLOX10 + })) { + LOG_I(TAG, "Configured internal GPS"); + } else { + LOG_E(TAG, "Failed to configure internal GPS"); + } + } + } + }); +} + +static error_t start() { + subscribe_events(); + return ERROR_NONE; +} + +static error_t stop() { + return ERROR_NONE; +} + +struct Module lilygo_tlora_pager_module = { + .name = "lilygo-tlora-pager", + .start = start, + .stop = stop, + .symbols = nullptr, + .internal = nullptr +}; + +} diff --git a/Documentation/ideas.md b/Documentation/ideas.md index c19f848f8..df4924f15 100644 --- a/Documentation/ideas.md +++ b/Documentation/ideas.md @@ -12,6 +12,7 @@ ## Higher Priority +- Create `#define` for empty module (for modules that fully rely on device.properties and don't define drivers or have start/stop logic) - Get rid of TactilityC in favour of TactilityKernel and kernel modules - Improve SPI kernel driver (implement read, write, transactions) - Add font design tokens such as "regular", "title" and "smaller". Perhaps via the LVGL kernel module. diff --git a/Drivers/BQ25896/CMakeLists.txt b/Drivers/BQ25896/CMakeLists.txt deleted file mode 100644 index 8074f3b38..000000000 --- a/Drivers/BQ25896/CMakeLists.txt +++ /dev/null @@ -1,5 +0,0 @@ -idf_component_register( - SRC_DIRS "Source" - INCLUDE_DIRS "Source" - REQUIRES Tactility -) diff --git a/Drivers/BQ25896/README.md b/Drivers/BQ25896/README.md deleted file mode 100644 index 5e77d4c70..000000000 --- a/Drivers/BQ25896/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# BQ25896 Power Management IC - -[Product page](https://www.ti.com/product/BQ25896) -[Data sheet](https://www.ti.com/lit/gpn/bq25896) -[Refence implementation](https://github.com/lewisxhe/XPowersLib/blob/73b92a6641b72c0aca2be989207689cb05da9788/src/PowersBQ25896.tpp) diff --git a/Drivers/BQ25896/Source/Bq25896.cpp b/Drivers/BQ25896/Source/Bq25896.cpp deleted file mode 100644 index c15169e97..000000000 --- a/Drivers/BQ25896/Source/Bq25896.cpp +++ /dev/null @@ -1,15 +0,0 @@ -#include "Bq25896.h" - -#include - -constexpr auto* TAG = "BQ25896"; - -void Bq25896::powerOff() { - LOG_I(TAG, "Power off"); - bitOn(0x09, BIT(5)); -} - -void Bq25896::powerOn() { - LOG_I(TAG, "Power on"); - bitOff(0x09, BIT(5)); -} diff --git a/Drivers/BQ25896/Source/Bq25896.h b/Drivers/BQ25896/Source/Bq25896.h deleted file mode 100644 index 0e85a5739..000000000 --- a/Drivers/BQ25896/Source/Bq25896.h +++ /dev/null @@ -1,22 +0,0 @@ -#pragma once - -#include - -constexpr auto BQ25896_ADDRESS = 0x6b; - -class Bq25896 final : public tt::hal::i2c::I2cDevice { - -public: - - explicit Bq25896(::Device* controller) : I2cDevice(controller, BQ25896_ADDRESS) { - powerOn(); - } - - std::string getName() const override { return "BQ25896"; } - - std::string getDescription() const override { return "I2C 1 cell 3A buck battery charger with power path and PSEL"; } - - void powerOff(); - - void powerOn(); -}; diff --git a/Drivers/BQ27220/CMakeLists.txt b/Drivers/BQ27220/CMakeLists.txt deleted file mode 100644 index 8074f3b38..000000000 --- a/Drivers/BQ27220/CMakeLists.txt +++ /dev/null @@ -1,5 +0,0 @@ -idf_component_register( - SRC_DIRS "Source" - INCLUDE_DIRS "Source" - REQUIRES Tactility -) diff --git a/Drivers/BQ27220/README.md b/Drivers/BQ27220/README.md deleted file mode 100644 index 6a9240a58..000000000 --- a/Drivers/BQ27220/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# BQ27220 - -Power management: Single-Cell CEDV Fuel Gauge - -[Datasheet](https://www.ti.com/lit/gpn/bq27220) -[User Guide](https://www.ti.com/lit/pdf/sluubd4) diff --git a/Drivers/BQ27220/Source/Bq27220.cpp b/Drivers/BQ27220/Source/Bq27220.cpp deleted file mode 100644 index 8a40caab4..000000000 --- a/Drivers/BQ27220/Source/Bq27220.cpp +++ /dev/null @@ -1,370 +0,0 @@ -#include "Bq27220.h" -#include - -#include "esp_sleep.h" - -constexpr auto* TAG = "BQ27220"; - -#define ARRAYSIZE(a) (sizeof(a) / sizeof(*(a))) - -static uint8_t highByte(const uint16_t word) { return (word >> 8) & 0xFF; } -static uint8_t lowByte(const uint16_t word) { return word & 0xFF; } -static constexpr void swapEndianess(uint16_t &word) { word = (lowByte(word) << 8) | highByte(word); } - -namespace registers { - static const uint16_t SUBCMD_CTRL_STATUS = 0x0000U; - static const uint16_t SUBCMD_DEVICE_NUMBER = 0x0001U; - static const uint16_t SUBCMD_FW_VERSION = 0x0002U; - static const uint16_t SUBCMD_HW_VERSION = 0x0003U; - static const uint16_t SUBCMD_BOARD_OFFSET = 0x0009U; - static const uint16_t SUBCMD_CC_OFFSET = 0x000AU; - static const uint16_t SUBCMD_CC_OFFSET_SAVE = 0x000BU; - static const uint16_t SUBCMD_OCV_CMD = 0x000CU; - static const uint16_t SUBCMD_BAT_INSERT = 0x000DU; - static const uint16_t SUBCMD_BAT_REMOVE = 0x000EU; - static const uint16_t SUBCMD_SET_SNOOZE = 0x0013U; - static const uint16_t SUBCMD_CLEAR_SNOOZE = 0x0014U; - static const uint16_t SUBCMD_SET_PROFILE_1 = 0x0015U; - static const uint16_t SUBCMD_SET_PROFILE_2 = 0x0016U; - static const uint16_t SUBCMD_SET_PROFILE_3 = 0x0017U; - static const uint16_t SUBCMD_SET_PROFILE_4 = 0x0018U; - static const uint16_t SUBCMD_SET_PROFILE_5 = 0x0019U; - static const uint16_t SUBCMD_SET_PROFILE_6 = 0x001AU; - static const uint16_t SUBCMD_CAL_TOGGLE = 0x002DU; - static const uint16_t SUBCMD_SEALED = 0x0030U; - static const uint16_t SUBCMD_RESET = 0x0041U; - static const uint16_t SUBCMD_EXIT_CAL = 0x0080U; - static const uint16_t SUBCMD_ENTER_CAL = 0x0081U; - static const uint16_t SUBCMD_ENTER_CFG_UPDATE = 0x0090U; - static const uint16_t SUBCMD_EXIT_CFG_UPDATE_REINIT = 0x0091U; - static const uint16_t SUBCMD_EXIT_CFG_UPDATE = 0x0092U; - static const uint16_t SUBCMD_RETURN_TO_ROM = 0x0F00U; - - static const uint8_t CMD_CONTROL = 0x00U; - static const uint8_t CMD_AT_RATE = 0x02U; - static const uint8_t CMD_AT_RATE_TIME_TO_EMPTY = 0x04U; - static const uint8_t CMD_TEMPERATURE = 0x06U; - static const uint8_t CMD_VOLTAGE = 0x08U; - static const uint8_t CMD_BATTERY_STATUS = 0x0AU; - static const uint8_t CMD_CURRENT = 0x0CU; - static const uint8_t CMD_REMAINING_CAPACITY = 0x10U; - static const uint8_t CMD_FULL_CHARGE_CAPACITY = 0x12U; - static const uint8_t CMD_AVG_CURRENT = 0x14U; - static const uint8_t CMD_TIME_TO_EMPTY = 0x16U; - static const uint8_t CMD_TIME_TO_FULL = 0x18U; - static const uint8_t CMD_STANDBY_CURRENT = 0x1AU; - static const uint8_t CMD_STANDBY_TIME_TO_EMPTY = 0x1CU; - static const uint8_t CMD_MAX_LOAD_CURRENT = 0x1EU; - static const uint8_t CMD_MAX_LOAD_TIME_TO_EMPTY = 0x20U; - static const uint8_t CMD_RAW_COULOMB_COUNT = 0x22U; - static const uint8_t CMD_AVG_POWER = 0x24U; - static const uint8_t CMD_INTERNAL_TEMPERATURE = 0x28U; - static const uint8_t CMD_CYCLE_COUNT = 0x2AU; - static const uint8_t CMD_STATE_OF_CHARGE = 0x2CU; - static const uint8_t CMD_STATE_OF_HEALTH = 0x2EU; - static const uint8_t CMD_CHARGE_VOLTAGE = 0x30U; - static const uint8_t CMD_CHARGE_CURRENT = 0x32U; - static const uint8_t CMD_BTP_DISCHARGE_SET = 0x34U; - static const uint8_t CMD_BTP_CHARGE_SET = 0x36U; - static const uint8_t CMD_OPERATION_STATUS = 0x3AU; - static const uint8_t CMD_DESIGN_CAPACITY = 0x3CU; - static const uint8_t CMD_SELECT_SUBCLASS = 0x3EU; - static const uint8_t CMD_MAC_DATA = 0x40U; - static const uint8_t CMD_MAC_DATA_SUM = 0x60U; - static const uint8_t CMD_MAC_DATA_LEN = 0x61U; - static const uint8_t CMD_ANALOG_COUNT = 0x79U; - static const uint8_t CMD_RAW_CURRENT = 0x7AU; - static const uint8_t CMD_RAW_VOLTAGE = 0x7CU; - static const uint8_t CMD_RAW_INTERNAL_TEMPERATURE = 0x7EU; - static const uint8_t MAC_BUFFER_START = 0x40U; - static const uint8_t MAC_BUFFER_END = 0x5FU; - static const uint8_t MAC_DATA_SUM = 0x60U; - static const uint8_t MAC_DATA_LEN = 0x61U; - static const uint8_t ROM_START = 0x3EU; - - static const uint16_t ROM_FULL_CHARGE_CAPACITY = 0x929DU; - static const uint16_t ROM_DESIGN_CAPACITY = 0x929FU; - static const uint16_t ROM_OPERATION_CONFIG_A = 0x9206U; - static const uint16_t ROM_OPERATION_CONFIG_B = 0x9208U; - -} // namespace registers - -bool Bq27220::configureCapacity(uint16_t designCapacity, uint16_t fullChargeCapacity) { - return performConfigUpdate([this, designCapacity, fullChargeCapacity]() { - // Set the design capacity - if (!writeConfig16(registers::ROM_DESIGN_CAPACITY, designCapacity)) { - LOG_E(TAG, "Failed to set design capacity!"); - return false; - } - vTaskDelay(10 / portTICK_PERIOD_MS); - - // Set full charge capacity - if (!writeConfig16(registers::ROM_FULL_CHARGE_CAPACITY, fullChargeCapacity)) { - LOG_E(TAG, "Failed to set full charge capacity!"); - return false; - } - vTaskDelay(10 / portTICK_PERIOD_MS); - - return true; - }); -} - -bool Bq27220::getVoltage(uint16_t &value) { - if (readRegister16(registers::CMD_VOLTAGE, value)) { - swapEndianess(value); - return true; - } - return false; -} - -bool Bq27220::getCurrent(int16_t &value) { - uint16_t u16 = 0; - if (readRegister16(registers::CMD_CURRENT, u16)) { - swapEndianess(u16); - value = (int16_t)u16; - return true; - } - return false; -} - -bool Bq27220::getBatteryStatus(BatteryStatus &batt_sta) { - if (readRegister16(registers::CMD_BATTERY_STATUS, batt_sta.full)) { - swapEndianess(batt_sta.full); - return true; - } - return false; -} - -bool Bq27220::getOperationStatus(OperationStatus &oper_sta) { - if (readRegister16(registers::CMD_OPERATION_STATUS, oper_sta.full)) { - swapEndianess(oper_sta.full); - return true; - } - return false; -} - -bool Bq27220::getTemperature(uint16_t &value) { - if (readRegister16(registers::CMD_TEMPERATURE, value)) { - swapEndianess(value); - return true; - } - return false; -} - -bool Bq27220::getFullChargeCapacity(uint16_t &value) { - if (readRegister16(registers::CMD_FULL_CHARGE_CAPACITY, value)) { - swapEndianess(value); - return true; - } - return false; -} - -bool Bq27220::getDesignCapacity(uint16_t &value) { - if (readRegister16(registers::CMD_DESIGN_CAPACITY, value)) { - swapEndianess(value); - return true; - } - return false; -} - -bool Bq27220::getRemainingCapacity(uint16_t &value) { - if (readRegister16(registers::CMD_REMAINING_CAPACITY, value)) { - swapEndianess(value); - return true; - } - return false; -} - -bool Bq27220::getStateOfCharge(uint16_t &value) { - if (readRegister16(registers::CMD_STATE_OF_CHARGE, value)) { - swapEndianess(value); - return true; - } - return false; -} - -bool Bq27220::getStateOfHealth(uint16_t &value) { - if (readRegister16(registers::CMD_STATE_OF_HEALTH, value)) { - swapEndianess(value); - return true; - } - return false; -} - -bool Bq27220::getChargeVoltageMax(uint16_t &value) { - if (readRegister16(registers::CMD_CHARGE_VOLTAGE, value)) { - swapEndianess(value); - return true; - } - return false; -} - -bool Bq27220::unsealDevice() { - uint8_t cmd1[] = {0x00, 0x14, 0x04}; - if (!write(cmd1, ARRAYSIZE(cmd1))) { - return false; - } - vTaskDelay(50 / portTICK_PERIOD_MS); - uint8_t cmd2[] = {0x00, 0x72, 0x36}; - if (!write(cmd2, ARRAYSIZE(cmd2))) { - return false; - } - vTaskDelay(50 / portTICK_PERIOD_MS); - return true; -} - -bool Bq27220::unsealFullAccess() -{ - uint8_t buffer[3]; - buffer[0] = 0x00; - buffer[1] = lowByte((accessKey >> 24)); - buffer[2] = lowByte((accessKey >> 16)); - if (!write(buffer, ARRAYSIZE(buffer))) { - return false; - } - vTaskDelay(50 / portTICK_PERIOD_MS); - buffer[1] = lowByte((accessKey >> 8)); - buffer[2] = lowByte((accessKey)); - if (!write(buffer, ARRAYSIZE(buffer))) { - return false; - } - vTaskDelay(50 / portTICK_PERIOD_MS); - return true; -} - -bool Bq27220::exitSealMode() { - return sendSubCommand(registers::SUBCMD_SEALED); -} - -bool Bq27220::sendSubCommand(uint16_t subCmd, bool waitConfirm) -{ - uint8_t buffer[3]; - buffer[0] = 0x00; - buffer[1] = lowByte(subCmd); - buffer[2] = highByte(subCmd); - if (!write(buffer, ARRAYSIZE(buffer))) { - return false; - } - if (!waitConfirm) { - vTaskDelay(10 / portTICK_PERIOD_MS); - return true; - } - constexpr uint8_t statusReg = 0x00; - int waitCount = 20; - vTaskDelay(10 / portTICK_PERIOD_MS); - while (waitCount--) { - writeRead(&statusReg, 1, buffer, 2); - uint16_t *value = reinterpret_cast(buffer); - if (*value == 0xFFA5) { - return true; - } - vTaskDelay(100 / portTICK_PERIOD_MS); - } - LOG_E(TAG, "Subcommand 0x%04X failed!", subCmd); - return false; -} - -bool Bq27220::writeConfig16(uint16_t address, uint16_t value) { - constexpr uint8_t fixedDataLength = 0x06; - const uint8_t msbAccessValue = highByte(address); - const uint8_t lsbAccessValue = lowByte(address); - - // Write to access the MSB of Capacity - writeRegister8(registers::ROM_START, msbAccessValue); - - // Write to access the LSB of Capacity - writeRegister8(registers::ROM_START + 1, lsbAccessValue); - - // Write two Capacity bytes starting from 0x40 - uint8_t valueMsb = highByte(value); - uint8_t valueLsb = lowByte(value); - uint8_t configRaw[] = {valueMsb, valueLsb}; - writeRegister(registers::MAC_BUFFER_START, configRaw, 2); - // Calculate new checksum - uint8_t checksum = 0xFF - ((msbAccessValue + lsbAccessValue + valueMsb + valueLsb) & 0xFF); - - // Write new checksum (0x60) - writeRegister8(registers::MAC_DATA_SUM, checksum); - - // Write the block length - writeRegister8(registers::MAC_DATA_LEN, fixedDataLength); - - return true; -} - -bool Bq27220::configPreamble(bool &isSealed) { - int timeout = 0; - OperationStatus status; - - // Check access settings - if(!getOperationStatus(status)) { - LOG_E(TAG, "Cannot read initial operation status!"); - return false; - } - - if (status.reg.SEC == OperationStatusSecSealed) { - isSealed = true; - if (!unsealDevice()) { - LOG_E(TAG, "Unsealing device failure!"); - return false; - } - } - - if (status.reg.SEC != OperationStatusSecFull) { - if (!unsealFullAccess()) { - LOG_E(TAG, "Unsealing full access failure!"); - return false; - } - } - - // Send ENTER_CFG_UPDATE command (0x0090) - if (!sendSubCommand(registers::SUBCMD_ENTER_CFG_UPDATE)) { - LOG_E(TAG, "Config Update Subcommand failure!"); - } - - // Confirm CFUPDATE mode by polling the OperationStatus() register until Bit 2 is set. - bool isConfigUpdate = false; - for (timeout = 30; timeout; --timeout) { - getOperationStatus(status); - if (status.reg.CFGUPDATE) { - isConfigUpdate = true; - break; - } - vTaskDelay(100 / portTICK_PERIOD_MS); - } - if (!isConfigUpdate) { - LOG_E(TAG, "Update Mode timeout, maybe the access key for full permissions is invalid!"); - return false; - } - - return true; -} - -bool Bq27220::configEpilouge(const bool isSealed) { - int timeout = 0; - OperationStatus status; - - // Exit CFUPDATE mode by sending the EXIT_CFG_UPDATE_REINIT (0x0091) or EXIT_CFG_UPDATE (0x0092) command - sendSubCommand(registers::SUBCMD_EXIT_CFG_UPDATE_REINIT); - vTaskDelay(10 / portTICK_PERIOD_MS); - - // Confirm that CFUPDATE mode has been exited by polling the OperationStatus() register until bit 2 is cleared - for (timeout = 60; timeout; --timeout) { - getOperationStatus(status); - if (!status.reg.CFGUPDATE) { - break; - } - vTaskDelay(100 / portTICK_PERIOD_MS); - } - if (timeout == 0) { - LOG_E(TAG, "Timed out waiting to exit update mode."); - return false; - } - - // If the device was previously in SEALED state, return to SEALED mode by sending the Control(0x0030) subcommand - if (isSealed) { - LOG_D(TAG, "Restore Safe Mode!"); - exitSealMode(); - } - return true; -} diff --git a/Drivers/BQ27220/Source/Bq27220.h b/Drivers/BQ27220/Source/Bq27220.h deleted file mode 100644 index 6f9017d21..000000000 --- a/Drivers/BQ27220/Source/Bq27220.h +++ /dev/null @@ -1,106 +0,0 @@ -#pragma once - -#include - -#define BQ27220_ADDRESS 0x55 - -class Bq27220 final : public tt::hal::i2c::I2cDevice { - - uint32_t accessKey; - - bool unsealDevice(); - bool unsealFullAccess(); - bool exitSealMode(); - bool sendSubCommand(uint16_t subCmd, bool waitConfirm = false); - bool writeConfig16(uint16_t address, uint16_t value); - bool configPreamble(bool &isSealed); - bool configEpilouge(bool isSealed); - - template - bool performConfigUpdate(T configUpdateFunc) - { - bool isSealed = false; - - if (!configPreamble(isSealed)) { - return false; - } - bool result = configUpdateFunc(); - configEpilouge(isSealed); - - return result; - } - -public: - // Register structures lifted from - // https://github.com/Xinyuan-LilyGO/T-Deck-Pro/blob/master/lib/BQ27220/bq27220.h - // Copyright (c) 2025 Liygo / Shenzhen Xinyuan Electronic Technology Co., Ltd - - union BatteryStatus { - struct - { - // Low byte, Low bit first - uint16_t DSG : 1; /**< The device is in DISCHARGE */ - uint16_t SYSDWN : 1; /**< System down bit indicating the system should shut down */ - uint16_t TDA : 1; /**< Terminate Discharge Alarm */ - uint16_t BATTPRES : 1; /**< Battery Present detected */ - uint16_t AUTH_GD : 1; /**< Detect inserted battery */ - uint16_t OCVGD : 1; /**< Good OCV measurement taken */ - uint16_t TCA : 1; /**< Terminate Charge Alarm */ - uint16_t RSVD : 1; /**< Reserved */ - // High byte, Low bit first - uint16_t CHGING : 1; /**< Charge inhibit */ - uint16_t FC : 1; /**< Full-charged is detected */ - uint16_t OTD : 1; /**< Overtemperature in discharge condition is detected */ - uint16_t OTC : 1; /**< Overtemperature in charge condition is detected */ - uint16_t SLEEP : 1; /**< Device is operating in SLEEP mode when set */ - uint16_t OCVFALL : 1; /**< Status bit indicating that the OCV reading failed due to current */ - uint16_t OCVCOMP : 1; /**< An OCV measurement update is complete */ - uint16_t FD : 1; /**< Full-discharge is detected */ - } reg; - uint16_t full; - }; - - enum OperationStatusSec { - OperationStatusSecSealed = 0b11, - OperationStatusSecUnsealed = 0b10, - OperationStatusSecFull = 0b01, - }; - - union OperationStatus { - struct - { - // Low byte, Low bit first - bool CALMD : 1; /**< Calibration mode enabled */ - uint8_t SEC : 2; /**< Current security access */ - bool EDV2 : 1; /**< EDV2 threshold exceeded */ - bool VDQ : 1; /**< Indicates if Current discharge cycle is NOT qualified or qualified for an FCC updated */ - bool INITCOMP : 1; /**< gauge initialization is complete */ - bool SMTH : 1; /**< RemainingCapacity is scaled by smooth engine */ - bool BTPINT : 1; /**< BTP threshold has been crossed */ - // High byte, Low bit first - uint8_t RSVD1 : 2; /**< Reserved */ - bool CFGUPDATE : 1; /**< Gauge is in CONFIG UPDATE mode */ - uint8_t RSVD0 : 5; /**< Reserved */ - } reg; - uint16_t full; - }; - - std::string getName() const override { return "BQ27220"; } - - std::string getDescription() const override { return "I2C-controlled CEDV battery fuel gauge"; } - - explicit Bq27220(::Device* controller) : I2cDevice(controller, BQ27220_ADDRESS), accessKey(0xFFFFFFFF) {} - - bool configureCapacity(uint16_t designCapacity, uint16_t fullChargeCapacity); - bool getVoltage(uint16_t &value); - bool getCurrent(int16_t &value); - bool getBatteryStatus(BatteryStatus &batt_sta); - bool getOperationStatus(OperationStatus &oper_sta); - bool getTemperature(uint16_t &value); - bool getFullChargeCapacity(uint16_t &value); - bool getDesignCapacity(uint16_t &value); - bool getRemainingCapacity(uint16_t &value); - bool getStateOfCharge(uint16_t &value); - bool getStateOfHealth(uint16_t &value); - bool getChargeVoltageMax(uint16_t &value); -}; diff --git a/Drivers/DRV2605/CMakeLists.txt b/Drivers/DRV2605/CMakeLists.txt deleted file mode 100644 index 8074f3b38..000000000 --- a/Drivers/DRV2605/CMakeLists.txt +++ /dev/null @@ -1,5 +0,0 @@ -idf_component_register( - SRC_DIRS "Source" - INCLUDE_DIRS "Source" - REQUIRES Tactility -) diff --git a/Drivers/DRV2605/README.md b/Drivers/DRV2605/README.md deleted file mode 100644 index ec4035c75..000000000 --- a/Drivers/DRV2605/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# DRV2605 Haptic Driver - -[Datasheet](https://www.ti.com/product/DRV2605) -[Reference implementation](https://github.com/lewisxhe/SensorLib/blob/master/src/SensorDRV2605.hpp) -[Usage in T-Lora Pager code from LilyGO](https://github.com/Xinyuan-LilyGO/LilyGoLib/blob/6b534a28b0ec31313e4a7e89c5e8b7e4437e6fd1/src/LilyGo_LoRa_Pager.cpp#L956) diff --git a/Drivers/DRV2605/Source/Drv2605.cpp b/Drivers/DRV2605/Source/Drv2605.cpp deleted file mode 100644 index 556e2fafb..000000000 --- a/Drivers/DRV2605/Source/Drv2605.cpp +++ /dev/null @@ -1,96 +0,0 @@ -#include "Drv2605.h" - -#include -#include - -constexpr auto* TAG = "DRV2605"; - -Drv2605::Drv2605(::Device* controller, bool autoPlayStartupBuzz) : I2cDevice(controller, ADDRESS), autoPlayStartupBuzz(autoPlayStartupBuzz) { - check(init(), "Initialize DRV2605"); - - if (autoPlayStartupBuzz) { - setWaveFormForBuzz(); - startPlayback(); - } -} - -bool Drv2605::init() { - uint8_t status; - if (!readRegister8(static_cast(Register::Status), status)) { - LOG_E(TAG, "Failed to read status"); - return false; - } - status >>= 5; - - ChipId chip_id = static_cast(status); - if (chip_id != ChipId::DRV2604 && chip_id != ChipId::DRV2604L && chip_id != ChipId::DRV2605 && chip_id != ChipId::DRV2605L) { - LOG_E(TAG, "Unknown chip id %02X", static_cast(chip_id)); - return false; - } - - writeRegister(Register::Mode, 0x00); // Get out of standby - - writeRegister(Register::RealtimePlaybackInput, 0x00); // Disable - - - setWaveFormForClick(); - - // ERM open loop - - uint8_t feedback; - if (!readRegister(Register::Feedback, feedback)) { - LOG_E(TAG, "Failed to read feedback"); - return false; - } - - writeRegister(Register::Feedback, feedback & 0x7F); // N_ERM_LRA off - - bitOnByIndex(static_cast(Register::Control3), 5); // ERM_OPEN_LOOP on - - return true; -} - -void Drv2605::setWaveFormForBuzz() { - writeRegister(Register::WaveSequence1, 1); // Strong click - writeRegister(Register::WaveSequence2, 1); // Strong click - writeRegister(Register::WaveSequence3, 1); // Strong click - writeRegister(Register::WaveSequence4, 0); // End sequence - - writeRegister(Register::OverdriveTimeOffset, 0); // No overdrive - - writeRegister(Register::SustainTimeOffsetPostivie, 0); - writeRegister(Register::SustainTimeOffsetNegative, 0); - writeRegister(Register::BrakeTimeOffset, 0); - - writeRegister(Register::AudioInputLevelMax, 0x64); -} - -void Drv2605::setWaveFormForClick() { - writeRegister(Register::WaveSequence1, 1); // Strong click - writeRegister(Register::WaveSequence2, 0); // End sequence - - writeRegister(Register::OverdriveTimeOffset, 0); // No overdrive - - writeRegister(Register::SustainTimeOffsetPostivie, 0); - writeRegister(Register::SustainTimeOffsetNegative, 0); - writeRegister(Register::BrakeTimeOffset, 0); - - writeRegister(Register::AudioInputLevelMax, 0x64); -} - -void Drv2605::setWaveForm(uint8_t slot, uint8_t waveform) { - writeRegister8(static_cast(Register::WaveSequence1) + slot, waveform); -} - -void Drv2605::selectLibrary(uint8_t library) { - writeRegister(Register::WaveLibrarySelect, library); -} - -void Drv2605::startPlayback() { - writeRegister(Register::Go, 0x01); -} - -void Drv2605::stopPlayback() { - writeRegister(Register::Go, 0x00); -} - diff --git a/Drivers/DRV2605/Source/Drv2605.h b/Drivers/DRV2605/Source/Drv2605.h deleted file mode 100644 index 11d265439..000000000 --- a/Drivers/DRV2605/Source/Drv2605.h +++ /dev/null @@ -1,83 +0,0 @@ -#pragma once - -#include - -class Drv2605 : public tt::hal::i2c::I2cDevice { - - static constexpr auto ADDRESS = 0x5A; - - bool autoPlayStartupBuzz; - - // Chip IDs - enum class ChipId { - DRV2604 = 0x04, // Has RAM. Doesn't havew licensed ROM library. - DRV2605 = 0x03, // Has licensed ROM library. Doesn't have RAM. - DRV2604L = 0x06, // Low-voltage variant of the DRV2604. - DRV2605L = 0x07 // Low-voltage variant of the DRV2605. - }; - - enum class Register { - Status = 0x00, - Mode = 0x01, - RealtimePlaybackInput = 0x02, - WaveLibrarySelect = 0x03, - WaveSequence1 = 0x04, - WaveSequence2 = 0x05, - WaveSequence3 = 0x06, - WaveSequence4 = 0x07, - WaveSequence5 = 0x08, - WaveSequence6 = 0x09, - WaveSequence7 = 0x0A, - WaveSequence8 = 0x0B, - Go = 0x0C, - OverdriveTimeOffset = 0x0D, - SustainTimeOffsetPostivie = 0x0E, - SustainTimeOffsetNegative = 0x0F, - BrakeTimeOffset = 0x10, - AudioControl = 0x11, - AudioInputLevelMin = 0x12, - AudioInputLevelMax = 0x13, - AudioOutputLevelMin = 0x14, - AudioOutputLevelMax = 0x15, - RatedVoltage = 0x16, - OverdriveClampVoltage = 0x17, - AutoCalibrationCompensation = 0x18, - AutoCalibrationBackEmf = 0x19, - Feedback = 0x1A, - Control1 = 0x1B, - Control2 = 0x1C, - Control3 = 0x1D, - Control4 = 0x1E, - Vbat = 0x21, - LraResonancePeriod = 0x22, - }; - - bool writeRegister(Register reg, const uint8_t value) const { - return writeRegister8(static_cast(reg), value); - } - - bool readRegister(Register reg, uint8_t& value) const { - return readRegister8(static_cast(reg), value); - } - -public: - - explicit Drv2605(::Device* controller, bool autoPlayStartupBuzz = true); - - std::string getName() const final { return "DRV2605"; } - std::string getDescription() const final { return "Haptic driver for ERM/LRA with waveform library & auto-resonance tracking"; } - - bool init(); - - void setWaveFormForBuzz(); - void setWaveFormForClick(); - - /** - * @param slot a value from 0 to 7 - * @param waveform - */ - void setWaveForm(uint8_t slot, uint8_t waveform); - void selectLibrary(uint8_t library); - void startPlayback(); - void stopPlayback(); -}; diff --git a/Drivers/SSD1306/CMakeLists.txt b/Drivers/SSD1306/CMakeLists.txt deleted file mode 100644 index a9519cf46..000000000 --- a/Drivers/SSD1306/CMakeLists.txt +++ /dev/null @@ -1,5 +0,0 @@ -idf_component_register( - SRC_DIRS "Source" - INCLUDE_DIRS "Source" - REQUIRES Tactility driver EspLcdCompat esp_lcd lvgl -) diff --git a/Drivers/SSD1306/README.md b/Drivers/SSD1306/README.md deleted file mode 100644 index 13a651e2f..000000000 --- a/Drivers/SSD1306/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# SSD1306 - -A very custom ESP32 LVGL driver for SSD1306 displays. diff --git a/Drivers/SSD1306/Source/Ssd1306Display.cpp b/Drivers/SSD1306/Source/Ssd1306Display.cpp deleted file mode 100644 index 8630cde04..000000000 --- a/Drivers/SSD1306/Source/Ssd1306Display.cpp +++ /dev/null @@ -1,216 +0,0 @@ -#include "Ssd1306Display.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -constexpr auto* TAG = "Ssd1306Display"; - -// SSD1306 commands -#define SSD1306_CMD_SET_CLOCK 0xD5 -#define SSD1306_CMD_SET_CHARGE_PUMP 0x8D -#define SSD1306_CMD_SET_SEGMENT_REMAP 0xA0 -#define SSD1306_CMD_SET_COM_SCAN_DIR 0xC0 -#define SSD1306_CMD_SET_COM_PIN_CFG 0xDA -#define SSD1306_CMD_SET_CONTRAST 0x81 -#define SSD1306_CMD_SET_PRECHARGE 0xD9 -#define SSD1306_CMD_SET_VCOMH_DESELECT 0xDB -#define SSD1306_CMD_DISPLAY_NORMAL 0xA6 -#define SSD1306_CMD_DISPLAY_INVERT 0xA7 -#define SSD1306_CMD_DISPLAY_OFF 0xAE -#define SSD1306_CMD_DISPLAY_ON 0xAF -#define SSD1306_CMD_SET_MEMORY_MODE 0x20 -#define SSD1306_CMD_SET_COLUMN_RANGE 0x21 -#define SSD1306_CMD_SET_PAGE_RANGE 0x22 -#define SSD1306_CMD_SET_MULTIPLEX 0xA8 -#define SSD1306_CMD_SET_OFFSET 0xD3 -#define SSD1306_CMD_STOP_SCROLL 0x2E -#define SSD1306_CMD_SET_SCAN_DIRECTION_REVERSED 0xC8 - -// Helper to send I2C commands directly -static bool ssd1306_i2c_send_cmd(i2c_port_t port, uint8_t addr, uint8_t cmd) { - uint8_t data[2] = {0x00, cmd}; // 0x00 = command mode - esp_err_t ret = i2c_master_write_to_device(port, addr, data, sizeof(data), pdMS_TO_TICKS(1000)); - if (ret != ESP_OK) { - LOG_E(TAG, "Failed to send command 0x%02X: %d", cmd, (int)ret); - return false; - } - return true; -} - -bool Ssd1306Display::createIoHandle(esp_lcd_panel_io_handle_t& ioHandle) { - const esp_lcd_panel_io_i2c_config_t io_config = { - .dev_addr = configuration->deviceAddress, - .on_color_trans_done = nullptr, - .user_ctx = nullptr, - .control_phase_bytes = 1, - .dc_bit_offset = 6, - .lcd_cmd_bits = 0, - .lcd_param_bits = 0, - .flags = { - .dc_low_on_data = false, - .disable_control_phase = false, - }, - .scl_speed_hz = 0 - }; - - if (esp_lcd_new_panel_io_i2c(static_cast(configuration->port), &io_config, &ioHandle) != ESP_OK) { - LOG_E(TAG, "Failed to create IO handle"); - return false; - } - - return true; -} - -bool Ssd1306Display::createPanelHandle(esp_lcd_panel_io_handle_t ioHandle, esp_lcd_panel_handle_t& panelHandle) { - // Manual hardware reset with proper timing for Heltec V3 - if (configuration->resetPin != GPIO_NUM_NC) { - gpio_config_t reset_gpio_config = { - .pin_bit_mask = 1ULL << configuration->resetPin, - .mode = GPIO_MODE_OUTPUT, - .pull_up_en = GPIO_PULLUP_DISABLE, - .pull_down_en = GPIO_PULLDOWN_DISABLE, - .intr_type = GPIO_INTR_DISABLE, - }; - gpio_config(&reset_gpio_config); - - gpio_set_level(configuration->resetPin, 0); - vTaskDelay(pdMS_TO_TICKS(10)); - gpio_set_level(configuration->resetPin, 1); - vTaskDelay(pdMS_TO_TICKS(100)); - } - - // Create ESP-IDF panel (but don't call init - we'll do custom init) - esp_lcd_panel_dev_config_t panel_config = { - .reset_gpio_num = GPIO_NUM_NC, // Already handled above - .color_space = ESP_LCD_COLOR_SPACE_MONOCHROME, - .data_endian = LCD_RGB_DATA_ENDIAN_BIG, - .bits_per_pixel = 1, // Must be 1 for monochrome - .flags = { - .reset_active_high = false, - }, - .vendor_config = nullptr, - }; - -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) - esp_lcd_panel_ssd1306_config_t ssd1306_config = { - .height = static_cast(configuration->verticalResolution), - }; - panel_config.vendor_config = &ssd1306_config; -#endif - - if (esp_lcd_new_panel_ssd1306(ioHandle, &panel_config, &panelHandle) != ESP_OK) { - LOG_E(TAG, "Failed to create panel"); - return false; - } - - // Don't call esp_lcd_panel_init() - it doesn't configure correctly for Heltec V3! - // Instead, send our custom initialization sequence directly via I2C - - auto port = configuration->port; - auto addr = configuration->deviceAddress; - - LOG_I(TAG, "Sending Heltec V3 custom init sequence"); - - // Display off while configuring - ssd1306_i2c_send_cmd(port, addr, SSD1306_CMD_DISPLAY_OFF); - - // Set oscillator frequency (MUST come early in sequence) - ssd1306_i2c_send_cmd(port, addr, SSD1306_CMD_SET_CLOCK); - ssd1306_i2c_send_cmd(port, addr, 0xF0); // ~96 Hz - - // Set multiplex ratio - ssd1306_i2c_send_cmd(port, addr, SSD1306_CMD_SET_MULTIPLEX); - ssd1306_i2c_send_cmd(port, addr, configuration->verticalResolution - 1); - - ssd1306_i2c_send_cmd(port, addr, SSD1306_CMD_SET_OFFSET); - ssd1306_i2c_send_cmd(port, addr, 0x00); - ssd1306_i2c_send_cmd(port, addr, 0x40); - - // Enable charge pump (required for Heltec V3 - must be before memory mode) - ssd1306_i2c_send_cmd(port, addr, SSD1306_CMD_SET_CHARGE_PUMP); - ssd1306_i2c_send_cmd(port, addr, 0x14); // Enable - - // Horizontal addressing mode - ssd1306_i2c_send_cmd(port, addr, SSD1306_CMD_SET_MEMORY_MODE); - ssd1306_i2c_send_cmd(port, addr, 0x00); // Horizontal addressing - - // Segment remap (0xA1 for Heltec V3 orientation) - ssd1306_i2c_send_cmd(port, addr, SSD1306_CMD_SET_SEGMENT_REMAP | 0x01); - - ssd1306_i2c_send_cmd(port, addr, SSD1306_CMD_SET_SCAN_DIRECTION_REVERSED ); - - // COM pin configuration - ssd1306_i2c_send_cmd(port, addr, SSD1306_CMD_SET_COM_PIN_CFG); - if (configuration->verticalResolution == 64) { - ssd1306_i2c_send_cmd(port, addr, 0x12); // Alternative COM pin config for 64-row displays - } else { - ssd1306_i2c_send_cmd(port, addr, 0x02); // Sequential COM pin config for 32-row displays - } - - ssd1306_i2c_send_cmd(port, addr, SSD1306_CMD_SET_CONTRAST); - ssd1306_i2c_send_cmd(port, addr, 0xCF); - - ssd1306_i2c_send_cmd(port, addr, SSD1306_CMD_SET_PRECHARGE); - ssd1306_i2c_send_cmd(port, addr, 0xF1); - - ssd1306_i2c_send_cmd(port, addr, SSD1306_CMD_SET_VCOMH_DESELECT); - ssd1306_i2c_send_cmd(port, addr, 0x40); - - ssd1306_i2c_send_cmd(port, addr, SSD1306_CMD_SET_COLUMN_RANGE); - ssd1306_i2c_send_cmd(port, addr, 0); - ssd1306_i2c_send_cmd(port, addr, configuration->horizontalResolution - 1); - - ssd1306_i2c_send_cmd(port, addr, SSD1306_CMD_SET_PAGE_RANGE); - ssd1306_i2c_send_cmd(port, addr, 0); - if (configuration->verticalResolution == 64) - ssd1306_i2c_send_cmd(port, addr, 0x7); - else if (configuration->verticalResolution == 32) - ssd1306_i2c_send_cmd(port, addr, 0x3); - else - check(false, "Not supported"); - - ssd1306_i2c_send_cmd(port, addr, SSD1306_CMD_DISPLAY_INVERT); - ssd1306_i2c_send_cmd(port, addr, SSD1306_CMD_STOP_SCROLL); - ssd1306_i2c_send_cmd(port, addr, SSD1306_CMD_DISPLAY_ON); - - vTaskDelay(pdMS_TO_TICKS(100)); // Let display stabilize - - LOG_I(TAG, "Heltec V3 display initialized successfully"); - - return true; -} - -lvgl_port_display_cfg_t Ssd1306Display::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 = true, // ESP-LVGL-port handles the conversion! - .rotation = { - .swap_xy = false, - .mirror_x = false, - .mirror_y = false, - }, - .color_format = LV_COLOR_FORMAT_RGB565, // Use RGB565, monochrome flag makes it work! - .flags = { - .buff_dma = false, - .buff_spiram = false, - .sw_rotate = false, - .swap_bytes = false, - .full_refresh = true, - .direct_mode = false - } - }; -} diff --git a/Drivers/SSD1306/Source/Ssd1306Display.h b/Drivers/SSD1306/Source/Ssd1306Display.h deleted file mode 100644 index 115982beb..000000000 --- a/Drivers/SSD1306/Source/Ssd1306Display.h +++ /dev/null @@ -1,89 +0,0 @@ -#pragma once - -#include -#include - -#include -#include -#include - -class Ssd1306Display final : public EspLcdDisplay { - -public: - - class Configuration { - - public: - - Configuration( - i2c_port_t port, - uint8_t deviceAddress, - gpio_num_t resetPin, - unsigned int horizontalResolution, // Typically 128 - unsigned int verticalResolution, // 32 or 64 - std::shared_ptr touch = nullptr, - bool invertColor = false - ) : port(port), - deviceAddress(deviceAddress), - resetPin(resetPin), - horizontalResolution(horizontalResolution), - verticalResolution(verticalResolution), - invertColor(invertColor), - touch(std::move(touch)) - {} - - i2c_port_t port; - uint8_t deviceAddress; - gpio_num_t resetPin = GPIO_NUM_NC; - unsigned int horizontalResolution; - unsigned int verticalResolution; - bool invertColor = false; - std::shared_ptr touch; - uint32_t bufferSize = 0; // Size in pixel count. 0 means default (full screen / 8) - int gapX = 0; // Column offset - int gapY = 0; // Not used for SSD1306 - }; - -private: - - std::unique_ptr configuration; - - bool createIoHandle(esp_lcd_panel_io_handle_t& ioHandle) 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 Ssd1306Display(std::unique_ptr inConfiguration) : - configuration(std::move(inConfiguration)) - { - assert(configuration != nullptr); - if (configuration->bufferSize == 0) { - // For monochrome displays, ESP-LVGL-PORT expects full pixel count - // It handles the monochrome conversion internally - configuration->bufferSize = configuration->horizontalResolution * configuration->verticalResolution; - } - } - - std::string getName() const override { return "SSD1306"; } - - std::string getDescription() const override { return "SSD1306 monochrome OLED display with ESP-LVGL-PORT monochrome support"; } - - std::shared_ptr _Nullable getTouchDevice() override { return configuration->touch; } - - void setBacklightDuty(uint8_t backlightDuty) override { - // SSD1306 does not have backlight control - } - - bool supportsBacklightDuty() const override { return false; } - - void setGammaCurve(uint8_t index) override { - // SSD1306 does not support gamma curves - } - - uint8_t getGammaCurveCount() const override { return 0; } -}; - -std::shared_ptr createDisplay(); diff --git a/Drivers/ST7735/CMakeLists.txt b/Drivers/ST7735/CMakeLists.txt deleted file mode 100644 index b99e92e82..000000000 --- a/Drivers/ST7735/CMakeLists.txt +++ /dev/null @@ -1,5 +0,0 @@ -idf_component_register( - SRC_DIRS "Source" - INCLUDE_DIRS "Source" - REQUIRES Tactility driver EspLcdCompat esp_lcd_st7735 -) diff --git a/Drivers/ST7735/README.md b/Drivers/ST7735/README.md deleted file mode 100644 index eb3168974..000000000 --- a/Drivers/ST7735/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# ST7735 - -A basic ESP32 LVGL driver for ST7735 displays. diff --git a/Drivers/ST7735/Source/St7735Display.cpp b/Drivers/ST7735/Source/St7735Display.cpp deleted file mode 100644 index 3ef322b0c..000000000 --- a/Drivers/ST7735/Source/St7735Display.cpp +++ /dev/null @@ -1,170 +0,0 @@ -#include "St7735Display.h" - -#include - -#include -#include -#include -#include - -constexpr auto* TAG = "ST7735"; - -bool St7735Display::createIoHandle(esp_lcd_panel_io_handle_t& outHandle) { - LOG_I(TAG, "Starting"); - - 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 = 1, - .lsb_first = 0, - .cs_high_active = 0 - } - }; - - if (esp_lcd_new_panel_io_spi(configuration->spiHostDevice, &panel_io_config, &outHandle) != ESP_OK) { - LOG_E(TAG, "Failed to create panel"); - return false; - } - - return true; -} - -bool St7735Display::createPanelHandle(esp_lcd_panel_io_handle_t ioHandle, esp_lcd_panel_handle_t& panelHandle) { - - const esp_lcd_panel_dev_config_t panel_config = { - .reset_gpio_num = configuration->resetPin, - .rgb_ele_order = LCD_RGB_ELEMENT_ORDER_BGR, - .data_endian = LCD_RGB_DATA_ENDIAN_LITTLE, - .bits_per_pixel = 16, - .flags = { - .reset_active_high = false - }, - .vendor_config = nullptr - }; - - if (esp_lcd_new_panel_st7735(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_invert_color(panelHandle, configuration->invertColor) != ESP_OK) { - LOG_E(TAG, "Failed to set panel to invert"); - return false; - } - - // Warning: it looks like LVGL rotation is broken when "gap" is set and the screen is moved to a non-default orientation - int gap_x = configuration->swapXY ? configuration->gapY : configuration->gapX; - int gap_y = configuration->swapXY ? configuration->gapX : configuration->gapY; - if (esp_lcd_panel_set_gap(panelHandle, gap_x, gap_y) != ESP_OK) { - LOG_E(TAG, "Failed to set panel gap"); - 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_disp_on_off(panelHandle, true) != ESP_OK) { - LOG_E(TAG, "Failed to turn display on"); - return false; - } - - return true; -} - -lvgl_port_display_cfg_t St7735Display::getLvglPortDisplayConfig(esp_lcd_panel_io_handle_t ioHandle, esp_lcd_panel_handle_t panelHandle) { - return lvgl_port_display_cfg_t { - .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 St7735Display::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 - }; - - auto io_handle = getIoHandle(); - assert(io_handle != nullptr); - if (esp_lcd_panel_io_tx_param(io_handle, LCD_CMD_GAMSET, param, 1) != ESP_OK) { - LOG_E(TAG, "Failed to set gamma"); - } -} diff --git a/Drivers/ST7735/Source/St7735Display.h b/Drivers/ST7735/Source/St7735Display.h deleted file mode 100644 index 19420e677..000000000 --- a/Drivers/ST7735/Source/St7735Display.h +++ /dev/null @@ -1,109 +0,0 @@ -#pragma once - -#include -#include - -#include -#include -#include -#include -#include -#include - -class St7735Display 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 - int gapX = 0, - int gapY = 0 - ) : spiHostDevice(spiHostDevice), - csPin(csPin), - dcPin(dcPin), - resetPin(resetPin), - horizontalResolution(horizontalResolution), - verticalResolution(verticalResolution), - gapX(gapX), - gapY(gapY), - swapXY(swapXY), - mirrorX(mirrorX), - mirrorY(mirrorY), - invertColor(invertColor), - bufferSize(bufferSize), - 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 = GPIO_NUM_NC; - unsigned int pixelClockFrequency = 27'000'000; // Hertz - size_t transactionQueueDepth = 10; - unsigned int horizontalResolution; - unsigned int verticalResolution; - int gapX; - int gapY; - 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 - std::shared_ptr touch; - std::function _Nullable backlightDutyFunction = nullptr; - }; - -private: - - std::unique_ptr configuration; - - bool createIoHandle(esp_lcd_panel_io_handle_t& ioHandle) 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 St7735Display(std::unique_ptr inConfiguration) : - configuration(std::move(inConfiguration) - ) {} - - std::string getName() const override { return "ST7735"; } - - std::string getDescription() const override { return "ST7735 display"; } - - std::shared_ptr _Nullable 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; }; -}; - -std::shared_ptr createDisplay(); diff --git a/Drivers/ST7796/CMakeLists.txt b/Drivers/ST7796/CMakeLists.txt deleted file mode 100644 index f86c31217..000000000 --- a/Drivers/ST7796/CMakeLists.txt +++ /dev/null @@ -1,5 +0,0 @@ -idf_component_register( - SRC_DIRS "Source" - INCLUDE_DIRS "Source" - REQUIRES Tactility EspLcdCompat esp_lcd_st7796 driver -) diff --git a/Drivers/ST7796/README.md b/Drivers/ST7796/README.md deleted file mode 100644 index 5c8a5ee20..000000000 --- a/Drivers/ST7796/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# ST7796 - -A basic ESP32 LVGL driver for ST7796 displays. - diff --git a/Drivers/ST7796/Source/St7796Display.cpp b/Drivers/ST7796/Source/St7796Display.cpp deleted file mode 100644 index 36b7cd10b..000000000 --- a/Drivers/ST7796/Source/St7796Display.cpp +++ /dev/null @@ -1,173 +0,0 @@ -#include "St7796Display.h" - -#include - -#include -#include -#include - -constexpr auto* TAG = "ST7796"; - -bool St7796Display::createIoHandle(esp_lcd_panel_io_handle_t& ioHandle) { - 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, &ioHandle) == ESP_OK; -} - -bool St7796Display::createPanelHandle(esp_lcd_panel_io_handle_t ioHandle, esp_lcd_panel_handle_t& panelHandle) { - static const st7796_lcd_init_cmd_t lcd_init_cmds[] = { - {0x01, (uint8_t[]) {0x00}, 0, 120}, - {0x11, (uint8_t[]) {0x00}, 0, 120}, - {0xF0, (uint8_t[]) {0xC3}, 1, 0}, - {0xF0, (uint8_t[]) {0xC3}, 1, 0}, - {0xF0, (uint8_t[]) {0x96}, 1, 0}, - {0x36, (uint8_t[]) {0x58}, 1, 0}, - {0x3A, (uint8_t[]) {0x55}, 1, 0}, - {0xB4, (uint8_t[]) {0x01}, 1, 0}, - {0xB6, (uint8_t[]) {0x80, 0x02, 0x3B}, 3, 0}, - {0xE8, (uint8_t[]) {0x40, 0x8A, 0x00, 0x00, 0x29, 0x19, 0xA5, 0x33}, 8, 0}, - {0xC1, (uint8_t[]) {0x06}, 1, 0}, - {0xC2, (uint8_t[]) {0xA7}, 1, 0}, - {0xC5, (uint8_t[]) {0x18}, 1, 0}, - {0xE0, (uint8_t[]) {0xF0, 0x09, 0x0b, 0x06, 0x04, 0x15, 0x2F, 0x54, 0x42, 0x3C, 0x17, 0x14, 0x18, 0x1B}, 15, 0}, - {0xE1, (uint8_t[]) {0xE0, 0x09, 0x0b, 0x06, 0x04, 0x03, 0x2B, 0x43, 0x42, 0x3B, 0x16, 0x14, 0x17, 0x1B}, 15, 120}, - {0xF0, (uint8_t[]) {0x3C}, 1, 0}, - {0xF0, (uint8_t[]) {0x69}, 1, 0}, - {0x21, (uint8_t[]) {0x00}, 1, 0}, - {0x29, (uint8_t[]) {0x00}, 1, 0}, - }; - - st7796_vendor_config_t vendor_config = { - // Uncomment these lines if use custom initialization commands - .init_cmds = lcd_init_cmds, - .init_cmds_size = sizeof(lcd_init_cmds) / sizeof(st7796_lcd_init_cmd_t), - }; - - const esp_lcd_panel_dev_config_t panel_config = { - .reset_gpio_num = configuration->resetPin, // Set to -1 if not use - .color_space = LCD_RGB_ELEMENT_ORDER_RGB, - .data_endian = LCD_RGB_DATA_ENDIAN_LITTLE, - .bits_per_pixel = 16, - .vendor_config = &vendor_config - }; - - if (esp_lcd_new_panel_st7796(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_invert_color(panelHandle, configuration->invertColor) != ESP_OK) { - LOG_E(TAG, "Failed to set panel to invert"); - 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_set_gap(panelHandle, configuration->gapX, configuration->gapY) != ESP_OK) { - LOG_E(TAG, "Failed to set panel gap"); - return false; - } - - if (esp_lcd_panel_disp_on_off(panelHandle, true) != ESP_OK) { - LOG_E(TAG, "Failed to turn display on"); - return false; - } - - return true; -} - -lvgl_port_display_cfg_t St7796Display::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_NATIVE, - .flags = { - .buff_dma = true, - .buff_spiram = false, - .sw_rotate = false, - .swap_bytes = true, - .full_refresh = false, - .direct_mode = false - } - }; -} - -void St7796Display::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(ioHandle , LCD_CMD_GAMSET, param, 1) != ESP_OK) { - LOG_E(TAG, "Failed to set gamma"); - }*/ -} diff --git a/Drivers/ST7796/Source/St7796Display.h b/Drivers/ST7796/Source/St7796Display.h deleted file mode 100644 index 2f1097d46..000000000 --- a/Drivers/ST7796/Source/St7796Display.h +++ /dev/null @@ -1,105 +0,0 @@ -#pragma once - -#include -#include - -#include -#include - -class St7796Display final : public EspLcdDisplay { - -public: - - class Configuration { - - public: - - Configuration( - spi_host_device_t spiHostDevice, - gpio_num_t csPin, - gpio_num_t dcPin, - unsigned int horizontalResolution, - unsigned int verticalResolution, - std::shared_ptr touch, - bool swapXY = false, - bool mirrorX = false, - bool mirrorY = false, - bool invertColor = false, - unsigned int gapX = 0, - unsigned int gapY = 0, - uint32_t bufferSize = 0 // Size in pixel count. 0 means default, which is 1/10 of the screen size - ) : spiHostDevice(spiHostDevice), - csPin(csPin), - dcPin(dcPin), - horizontalResolution(horizontalResolution), - verticalResolution(verticalResolution), - swapXY(swapXY), - mirrorX(mirrorX), - mirrorY(mirrorY), - invertColor(invertColor), - gapX(gapX), - gapY(gapY), - bufferSize(bufferSize), - 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 = GPIO_NUM_NC; - unsigned int pixelClockFrequency = 80'000'000; // Hertz - size_t transactionQueueDepth = 2; - unsigned int horizontalResolution; - unsigned int verticalResolution; - bool swapXY = false; - bool mirrorX = false; - bool mirrorY = false; - bool invertColor = false; - unsigned int gapX = 0; - unsigned int gapY = 0; - uint32_t bufferSize = 0; // Size in pixel count. 0 means default, which is 1/10 of the screen size - std::shared_ptr touch; - std::function _Nullable backlightDutyFunction = nullptr; - }; - -private: - - std::unique_ptr configuration; - - bool createIoHandle(esp_lcd_panel_io_handle_t& ioHandle) 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 St7796Display(std::unique_ptr inConfiguration) : - configuration(std::move(inConfiguration) - ) { - assert(configuration != nullptr); - } - - std::string getName() const override { return "ST7796"; } - - std::string getDescription() const override { return "ST7796 display"; } - - std::shared_ptr _Nullable getTouchDevice() override { return configuration->touch; } - - void setBacklightDuty(uint8_t backlightDuty) override { - if (configuration->backlightDutyFunction != nullptr) { - configuration->backlightDutyFunction(backlightDuty); - } - } - - void setGammaCurve(uint8_t index) override; - - uint8_t getGammaCurveCount() const override { return 4; }; - - bool supportsBacklightDuty() const override { return configuration->backlightDutyFunction != nullptr; } -}; - -std::shared_ptr createDisplay(); diff --git a/Drivers/TCA8418/Adafruit_TCA8418-license.txt b/Drivers/TCA8418/Adafruit_TCA8418-license.txt deleted file mode 100644 index 5d943f1df..000000000 --- a/Drivers/TCA8418/Adafruit_TCA8418-license.txt +++ /dev/null @@ -1,26 +0,0 @@ -Software License Agreement (BSD License) - -Copyright (c) 2019 Limor Fried (Adafruit Industries) -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: -1. Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. -2. Redistributions in binary form must reproduce the above copyright -notice, this list of conditions and the following disclaimer in the -documentation and/or other materials provided with the distribution. -3. Neither the name of the copyright holders nor the -names of its contributors may be used to endorse or promote products -derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ''AS IS'' AND ANY -EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY -DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/Drivers/TCA8418/CMakeLists.txt b/Drivers/TCA8418/CMakeLists.txt deleted file mode 100644 index 8074f3b38..000000000 --- a/Drivers/TCA8418/CMakeLists.txt +++ /dev/null @@ -1,5 +0,0 @@ -idf_component_register( - SRC_DIRS "Source" - INCLUDE_DIRS "Source" - REQUIRES Tactility -) diff --git a/Drivers/TCA8418/ORIGINAL-COPYRIGHT.md b/Drivers/TCA8418/ORIGINAL-COPYRIGHT.md deleted file mode 100644 index dff105a94..000000000 --- a/Drivers/TCA8418/ORIGINAL-COPYRIGHT.md +++ /dev/null @@ -1,18 +0,0 @@ -Copyright 2023 Anthony DiGirolamo - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the “Software”), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Drivers/TCA8418/README.md b/Drivers/TCA8418/README.md deleted file mode 100644 index 7aefda0a4..000000000 --- a/Drivers/TCA8418/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# TCA8418 I2C Controlled Keypad Scan IC With Integrated ESD Protection - -[Datasheet](https://www.ti.com/lit/ds/symlink/tca8418.pdf?ts=1751500237439) -[Original implementation](https://github.com/AnthonyDiGirolamo/i2c-thumb-keyboard/tree/master) by Anthony DiGirolamo -[Adafruit TCA8418](https://github.com/adafruit/Adafruit_TCA8418) diff --git a/Drivers/TCA8418/Source/Tca8418.cpp b/Drivers/TCA8418/Source/Tca8418.cpp deleted file mode 100644 index 2e6e1b6aa..000000000 --- a/Drivers/TCA8418/Source/Tca8418.cpp +++ /dev/null @@ -1,227 +0,0 @@ -#include "Tca8418.h" - -namespace registers { -static const uint8_t CFG = 0x01U; -static const uint8_t KP_GPIO1 = 0x1DU; -static const uint8_t KP_GPIO2 = 0x1EU; -static const uint8_t KP_GPIO3 = 0x1FU; - -static const uint8_t KEY_EVENT_A = 0x04U; -static const uint8_t KEY_EVENT_B = 0x05U; -static const uint8_t KEY_EVENT_C = 0x06U; -static const uint8_t KEY_EVENT_D = 0x07U; -static const uint8_t KEY_EVENT_E = 0x08U; -static const uint8_t KEY_EVENT_F = 0x09U; -static const uint8_t KEY_EVENT_G = 0x0AU; -static const uint8_t KEY_EVENT_H = 0x0BU; -static const uint8_t KEY_EVENT_I = 0x0CU; -static const uint8_t KEY_EVENT_J = 0x0DU; -} // namespace registers - - -/** From https://github.com/adafruit/Adafruit_TCA8418/blob/main/Adafruit_TCA8418.cpp */ -bool Tca8418::initMatrix(uint8_t rows, uint8_t columns) { - if ((rows > 8) || (columns > 10)) - return false; - - if ((rows != 0) && (columns != 0)) { - // Configure the keypad matrix. - uint8_t mask = 0x00; - for (int r = 0; r < rows; r++) { - mask <<= 1; - mask |= 1; - } - writeRegister(registers::KP_GPIO1, &mask, 1); - - mask = 0x00; - for (int c = 0; c < columns && c < 8; c++) { - mask <<= 1; - mask |= 1; - } - writeRegister(registers::KP_GPIO2, &mask, 1); - - if (columns > 8) { - if (columns == 9) - mask = 0x01; - else - mask = 0x03; - writeRegister(registers::KP_GPIO3, &mask, 1); - } - } - - return true; -} -void Tca8418::init(uint8_t numrows, uint8_t numcols) { - /* - * | ADDRESS | REGISTER NAME | REGISTER DESCRIPTION | BIT7 | BIT6 | BIT5 | BIT4 | BIT3 | BIT2 | BIT1 | BIT0 | - * |---------+---------------+----------------------+------+------+------+------+------+------+------+------| - * | 0x1D | KP_GPIO1 | Keypad/GPIO Select 1 | ROW7 | ROW6 | ROW5 | ROW4 | ROW3 | ROW2 | ROW1 | ROW0 | - * | 0x1E | KP_GPIO2 | Keypad/GPIO Select 2 | COL7 | COL6 | COL5 | COL4 | COL3 | COL2 | COL1 | COL0 | - * | 0x1F | KP_GPIO3 | Keypad/GPIO Select 3 | N/A | N/A | N/A | N/A | N/A | N/A | COL9 | COL8 | - */ - - num_rows = numrows; - num_cols = numcols; - - initMatrix(num_rows, num_cols); - - /* - * BIT: NAME - * - * 7: AI - * Auto-increment for read and write operations; See below table for more information - * 0 = disabled - * 1 = enabled - * - * 6: GPI_E_CFG - * GPI event mode configuration - * 0 = GPI events are tracked when keypad is locked - * 1 = GPI events are not tracked when keypad is locked - * - * 5: OVR_FLOW_M - * Overflow mode - * 0 = disabled; Overflow data is lost - * 1 = enabled; Overflow data shifts with last event pushing first event out - * - * 4: INT_CFG - * Interrupt configuration - * 0 = processor interrupt remains asserted (or low) if host tries to clear interrupt while there is - * still a pending key press, key release or GPI interrupt - * 1 = processor interrupt is deasserted for 50 μs and reassert with pending interrupts - * - * 3: OVR_FLOW_IEN - * Overflow interrupt enable - * 0 = disabled; INT is not asserted if the FIFO overflows - * 1 = enabled; INT becomes asserted if the FIFO overflows - * - * 2: K_LCK_IEN - * Keypad lock interrupt enable - * 0 = disabled; INT is not asserted after a correct unlock key sequence - * 1 = enabled; INT becomes asserted after a correct unlock key sequence - * - * 1: GPI_IEN - * GPI interrupt enable to host processor - * 0 = disabled; INT is not asserted for a change on a GPI - * 1 = enabled; INT becomes asserted for a change on a GPI - * - * 0: KE_IEN - * Key events interrupt enable to host processor - * 0 = disabled; INT is not asserted when a key event occurs - * 1 = enabled; INT becomes asserted when a key event occurs - */ - - // 10111001 xB9 -- fifo overflow enabled - // 10011001 x99 -- fifo overflow disabled - writeRegister8(registers::CFG, 0x99); - - clear_released_list(); - clear_pressed_list(); -} - -bool Tca8418::update() { - last_update_micros = this_update_micros; - uint8_t key_down, key_event, key_row, key_col; - - key_event = get_key_event(); - // TODO: read gpio R7/R6 status? 0x14 bits 7&6 - // read(0x14, &new_keycode) - - // TODO: use tick function to get an update delta time - this_update_micros = 0; - delta_micros = this_update_micros - last_update_micros; - - if (key_event > 0) { - key_down = (key_event & 0x80); - uint16_t buffer = key_event; - buffer &= 0x7F; - buffer--; - key_row = buffer / 10; - key_col = buffer % 10; - - // always clear the released list - clear_released_list(); - - if (key_down) { - add_pressed_key(key_row, key_col); - // TODO reject ghosts (assume multiple key presses with the same hold time are ghosts.) - - } else { - add_released_key(key_row, key_col); - remove_pressed_key(key_row, key_col); - } - - return true; - } - - // Increment hold times for pressed keys - for (int i = 0; i < pressed_key_count; i++) { - pressed_list[i].hold_time += delta_micros; - } - - return false; -} - - -void Tca8418::add_pressed_key(uint8_t row, uint8_t col) { - if (pressed_key_count >= KEY_EVENT_LIST_SIZE) - return; - - pressed_list[pressed_key_count].row = row; - pressed_list[pressed_key_count].col = col; - pressed_list[pressed_key_count].hold_time = 0; - pressed_key_count++; -} - -void Tca8418::add_released_key(uint8_t row, uint8_t col) { - if (released_key_count >= KEY_EVENT_LIST_SIZE) - return; - - released_key_count++; - released_list[0].row = row; - released_list[0].col = col; -} - -void Tca8418::remove_pressed_key(uint8_t row, uint8_t col) { - if (pressed_key_count == 0) - return; - - // delete the pressed key - for (int i = 0; i < pressed_key_count; i++) { - if (pressed_list[i].row == row && - pressed_list[i].col == col) { - // shift remaining keys left one index - for (int j = i; i < pressed_key_count; j++) { - if (j == KEY_EVENT_LIST_SIZE - 1) - break; - pressed_list[j].row = pressed_list[j + 1].row; - pressed_list[j].col = pressed_list[j + 1].col; - pressed_list[j].hold_time = pressed_list[j + 1].hold_time; - } - break; - } - } - pressed_key_count--; -} - -void Tca8418::clear_pressed_list() { - for (int i = 0; i < KEY_EVENT_LIST_SIZE; i++) { - pressed_list[i].row = 255; - pressed_list[i].col = 255; - } - pressed_key_count = 0; -} - -void Tca8418::clear_released_list() { - for (int i = 0; i < KEY_EVENT_LIST_SIZE; i++) { - released_list[i].row = 255; - released_list[i].col = 255; - } - released_key_count = 0; -} - -uint8_t Tca8418::get_key_event() { - uint8_t new_keycode = 0; - - readRegister8(registers::KEY_EVENT_A, new_keycode); - return new_keycode; -} diff --git a/Drivers/TCA8418/Source/Tca8418.h b/Drivers/TCA8418/Source/Tca8418.h deleted file mode 100644 index a74688f50..000000000 --- a/Drivers/TCA8418/Source/Tca8418.h +++ /dev/null @@ -1,72 +0,0 @@ -#pragma once - -#include - -#include - -constexpr auto TCA8418_ADDRESS = 0x34U; -constexpr auto KEY_EVENT_LIST_SIZE = 10; - -/** - * See https://www.ti.com/lit/ds/symlink/tca8418.pdf - */ -class Tca8418 final : public tt::hal::i2c::I2cDevice { - - uint8_t tca8418_address; - uint32_t last_update_micros; - uint32_t this_update_micros; - - uint8_t new_pressed_keys_count; - - void clear_released_list(); - void clear_pressed_list(); - void add_pressed_key(uint8_t row, uint8_t col); - void add_released_key(uint8_t row, uint8_t col); - void remove_pressed_key(uint8_t row, uint8_t col); - void write(uint8_t register_address, uint8_t data); - bool read(uint8_t register_address, uint8_t* data); - - bool initMatrix(uint8_t rows, uint8_t columns); - -public: - - struct PressedKey { - uint8_t row; - uint8_t col; - uint32_t hold_time; - }; - - struct ReleasedKey { - uint8_t row; - uint8_t col; - }; - - std::string getName() const final { return "TCA8418"; } - - std::string getDescription() const final { return "I2C-controlled keyboard scan IC"; } - - explicit Tca8418(::Device* controller) : I2cDevice(controller, TCA8418_ADDRESS) { - delta_micros = 0; - last_update_micros = 0; - this_update_micros = 0; - } - - ~Tca8418() {} - - uint8_t num_rows; - uint8_t num_cols; - - uint32_t delta_micros; - - std::array pressed_list; - std::array released_list; - uint8_t pressed_key_count; - uint8_t released_key_count; - - void init(uint8_t numrows, uint8_t numcols); - bool update(); - uint8_t get_key_event(); - bool button_pressed(uint8_t row, uint8_t button_bit_position); - bool button_released(uint8_t row, uint8_t button_bit_position); - bool button_held(uint8_t row, uint8_t button_bit_position); -}; diff --git a/Drivers/TCA9534/.gitignore b/Drivers/TCA9534/.gitignore deleted file mode 100644 index dd9ca4594..000000000 --- a/Drivers/TCA9534/.gitignore +++ /dev/null @@ -1,56 +0,0 @@ -# Prerequisites -*.d - -# Object files -*.o -*.ko -*.obj -*.elf - -# Linker output -*.ilk -*.map -*.exp - -# Precompiled Headers -*.gch -*.pch - -# Libraries -*.lib -*.a -*.la -*.lo - -# Shared objects (inc. Windows DLLs) -*.dll -*.so -*.so.* -*.dylib - -# Executables -*.exe -*.out -*.app -*.i*86 -*.x86_64 -*.hex - -# Debug files -*.dSYM/ -*.su -*.idb -*.pdb - -# Kernel Module Compile Results -*.mod* -*.cmd -.tmp_versions/ -modules.order -Module.symvers -Mkfile.old -dkms.conf -.vscode/ -.idea/ -build/ -cmake-build-debug-esp-idf/ \ No newline at end of file diff --git a/Drivers/TCA9534/CMakeLists.txt b/Drivers/TCA9534/CMakeLists.txt deleted file mode 100644 index a835bf38d..000000000 --- a/Drivers/TCA9534/CMakeLists.txt +++ /dev/null @@ -1,7 +0,0 @@ -file(GLOB_RECURSE SOURCES src/*.c) - -idf_component_register( - SRCS ${SOURCES} - INCLUDE_DIRS include - REQUIRES driver -) diff --git a/Drivers/TCA9534/Kconfig.projbuild b/Drivers/TCA9534/Kconfig.projbuild deleted file mode 100644 index dc9568c77..000000000 --- a/Drivers/TCA9534/Kconfig.projbuild +++ /dev/null @@ -1,17 +0,0 @@ -menu "Example Configuration" - - config I2C_MASTER_SCL - int "SCL GPIO Num" - default 6 if IDF_TARGET_ESP32C3 - default 19 if IDF_TARGET_ESP32 || IDF_TARGET_ESP32S2 || IDF_TARGET_ESP32S3 - help - GPIO number for I2C Master clock line. - - config I2C_MASTER_SDA - int "SDA GPIO Num" - default 5 if IDF_TARGET_ESP32C3 - default 18 if IDF_TARGET_ESP32 || IDF_TARGET_ESP32S2 || IDF_TARGET_ESP32S3 - help - GPIO number for I2C Master data line. - -endmenu diff --git a/Drivers/TCA9534/LICENSE b/Drivers/TCA9534/LICENSE deleted file mode 100644 index 58a6c6cd5..000000000 --- a/Drivers/TCA9534/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2022 Victor Hogeweij - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/Drivers/TCA9534/Makefile b/Drivers/TCA9534/Makefile deleted file mode 100644 index f8be8b947..000000000 --- a/Drivers/TCA9534/Makefile +++ /dev/null @@ -1,8 +0,0 @@ -# -# This is a project Makefile. It is assumed the directory this Makefile resides in is a -# project subdirectory. -# - -PROJECT_NAME := i2c-simple - -include $(IDF_PATH)/make/project.mk diff --git a/Drivers/TCA9534/README.md b/Drivers/TCA9534/README.md deleted file mode 100644 index caac9b5b1..000000000 --- a/Drivers/TCA9534/README.md +++ /dev/null @@ -1,49 +0,0 @@ -# I2C Simple Example - -(See the README.md file in the upper level 'examples' directory for more information about examples.) - -## Overview - -This example demonstrates basic usage of I2C driver by reading and writing from a I2C connected sensor: - -If you have a new I2C application to go (for example, read the temperature data from external sensor with I2C interface), try this as a basic template, then add your own code. - -## How to use example - -### Hardware Required - -To run this example, you should have one ESP32, ESP32-S or ESP32-C based development board as well as a MPU9250. MPU9250 is a inertial measurement unit, which contains a accelerometer, gyroscope as well as a magnetometer, for more information about it, you can read the [PDF](https://invensense.tdk.com/wp-content/uploads/2015/02/PS-MPU-9250A-01-v1.1.pdf) of this sensor. - -#### Pin Assignment: - -**Note:** The following pin assignments are used by default, you can change these in the `menuconfig` . - -| | SDA | SCL | -| ---------------- | -------------- | -------------- | -| ESP I2C Master | I2C_MASTER_SDA | I2C_MASTER_SCL | -| MPU9250 Sensor | SDA | SCL | - - -For the actual default value of `I2C_MASTER_SDA` and `I2C_MASTER_SCL` see `Example Configuration` in `menuconfig`. - -**Note: ** There’s no need to add an external pull-up resistors for SDA/SCL pin, because the driver will enable the internal pull-up resistors. - -### Build and Flash - -Enter `idf.py -p PORT flash monitor` to build, flash and monitor the project. - -(To exit the serial monitor, type ``Ctrl-]``.) - -See the [Getting Started Guide](https://docs.espressif.com/projects/esp-idf/en/latest/get-started/index.html) for full steps to configure and use ESP-IDF to build projects. - -## Example Output - -```bash -I (328) i2c-simple-example: I2C initialized successfully -I (338) i2c-simple-example: WHO_AM_I = 71 -I (338) i2c-simple-example: I2C unitialized successfully -``` - -## Troubleshooting - -(For any technical queries, please open an [issue](https://github.com/espressif/esp-idf/issues) on GitHub. We will get back to you as soon as possible.) diff --git a/Drivers/TCA9534/component.mk b/Drivers/TCA9534/component.mk deleted file mode 100644 index 87e08136f..000000000 --- a/Drivers/TCA9534/component.mk +++ /dev/null @@ -1,3 +0,0 @@ -# -# Main Makefile. This is basically the same as a component makefile . -# diff --git a/Drivers/TCA9534/examples/CMakeLists.txt b/Drivers/TCA9534/examples/CMakeLists.txt deleted file mode 100644 index c2704e001..000000000 --- a/Drivers/TCA9534/examples/CMakeLists.txt +++ /dev/null @@ -1,6 +0,0 @@ -# The following lines of boilerplate have to be in your project's CMakeLists -# in this exact order for cmake to work correctly -cmake_minimum_required(VERSION 3.5) -set(EXTRA_COMPONENT_DIRS read) -include($ENV{IDF_PATH}/tools/cmake/project.cmake) -project(TCA9534_Examples) diff --git a/Drivers/TCA9534/examples/read/read_tca9534.c b/Drivers/TCA9534/examples/read/read_tca9534.c deleted file mode 100644 index b74511b77..000000000 --- a/Drivers/TCA9534/examples/read/read_tca9534.c +++ /dev/null @@ -1,59 +0,0 @@ -#include "esp_log.h" -#include "driver/i2c.h" -#include "TCA9534.h" - - -#define I2C_MASTER_SCL_IO CONFIG_I2C_MASTER_SCL /*!< GPIO number used for I2C master clock */ -#define I2C_MASTER_SDA_IO CONFIG_I2C_MASTER_SDA /*!< GPIO number used for I2C master data */ -#define I2C_MASTER_NUM 0 /*!< I2C master i2c port number, the number of i2c peripheral interfaces available will depend on the chip */ -#define I2C_MASTER_FREQ_HZ 400000 /*!< I2C master clock frequency */ -#define I2C_MASTER_TX_BUF_DISABLE 0 /*!< I2C master doesn't need buffer */ -#define I2C_MASTER_RX_BUF_DISABLE 0 /*!< I2C master doesn't need buffer */ - - -/** - * @brief i2c master initialization - */ -static esp_err_t i2c_master_init(i2c_config_t *conf) { - int i2c_master_port = I2C_MASTER_NUM; - - conf->mode = I2C_MODE_MASTER; - conf->master.clk_speed = I2C_MASTER_FREQ_HZ; - conf->sda_io_num = I2C_MASTER_SDA_IO; - conf->scl_io_num = I2C_MASTER_SCL_IO; - conf->sda_pullup_en = GPIO_PULLUP_ENABLE; - conf->scl_pullup_en = GPIO_PULLUP_ENABLE; - i2c_param_config(i2c_master_port, conf); - - return i2c_driver_install(i2c_master_port, conf->mode, I2C_MASTER_RX_BUF_DISABLE, I2C_MASTER_TX_BUF_DISABLE, 0); -} - - -static const char *TAG = "TCA9534-Example"; - -void app_main(void) { - TCA9534_IO_EXP IO_EXP1; - esp_err_t status = i2c_master_init(&IO_EXP1.i2c_conf); - if (status == ESP_OK) { - ESP_LOGI(TAG, "I2C initialized successfully"); - IO_EXP1.I2C_ADDR = 0b0100000; - IO_EXP1.i2c_master_port = I2C_MASTER_NUM; - - set_tca9534_io_pin_direction(IO_EXP1, TCA9534_IO0, TCA9534_INPUT); - set_tca9534_io_pin_direction(IO_EXP1, TCA9534_IO1, TCA9534_OUTPUT); - - int pin_state = 0; - while (1) { - pin_state = get_io_pin_input_status(IO_EXP1, TCA9534_IO0); - if (pin_state == -1) { - ESP_LOGE(TAG, "Cannot get pin status from TCA9534"); - break; - } - set_tca9534_io_pin_output_state(IO_EXP1, TCA9534_IO1, pin_state); - vTaskDelay(100 / portTICK_RATE_MS); - } - - ESP_ERROR_CHECK(i2c_driver_delete(I2C_MASTER_NUM)); - ESP_LOGI(TAG, "I2C unitialized successfully"); - } -} \ No newline at end of file diff --git a/Drivers/TCA9534/include/TCA9534.h b/Drivers/TCA9534/include/TCA9534.h deleted file mode 100644 index f9fac1fbd..000000000 --- a/Drivers/TCA9534/include/TCA9534.h +++ /dev/null @@ -1,170 +0,0 @@ -#ifndef TCA9534_IDF_TCA9534_H -#define TCA9534_IDF_TCA9534_H - -#include -#include - -#define TCA9534_ERROR -1 - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @brief TCA9534 IO Pins mapping - */ -typedef enum { - TCA9534_IO0, - TCA9534_IO1, - TCA9534_IO2, - TCA9534_IO3, - TCA9534_IO4, - TCA9534_IO5, - TCA9534_IO6, - TCA9534_IO7 -} TCA9534_PINS; - -/** - * @brief TCA9534 Port direction parameters - */ -typedef enum { - TCA9534_OUTPUT, - TCA9534_INPUT -} TCA9534_PORT_DIRECTION; - -/** - * @brief TCA9534 initialization parameters - */ -typedef struct { - uint8_t I2C_ADDR; - int i2c_master_port; - //Only when mode is set to interrupt, otherwise it won't be used.. - gpio_num_t interrupt_pin; - TaskHandle_t* interrupt_task; -} TCA9534_IO_EXP; - -/** - * @brief Setup interrupts using the builtin IO_EXP_INT pin of the tca9534 and interrupt handler+task - * @param io_exp which contains the gpio pin where IO_EXP_INT is connected(interrupt_pin) - * And optionally contains the task to run when interrupt triggered (interrupt_task) if not defined the - * default handler will be used. - */ -void setup_tca9534_interrupt_handler(TCA9534_IO_EXP* io_exp); - -/** - * @brief Get the current input state of the specified input pin (1 or 0) - * - * @param io_exp The io expander instance to read from or write to - * @param io_pin The io expander pin to read the state from - * - * @return - * - 0 Success! Pin is Low - * - 1 Success! Pin is High - * - TCA9534_ERROR(-1) Error! Something went wrong in the process of reading the io expander - */ -int16_t get_io_pin_input_status(TCA9534_IO_EXP* io_exp, TCA9534_PINS io_pin); - -/** - * @brief Get the current input state of all the io expander pins - * - * @param io_exp The io expander instance to read from or write to - * - * @return - * - 0x00 - 0xFF Success! Dump of input register, 1 bit is equal to 1 of the physical pins (Lower 8 bits of 16 bits result) - * - TCA9534_ERROR(-1) Error! Something went wrong in the process of reading the io expander - */ -int16_t get_tca9534_all_io_pin_input_status(TCA9534_IO_EXP* io_exp); - -/** - * @brief Get the current direction of all the io expander pins - * - * @param io_exp The io expander instance to read from or write to - * - * @return - * - 0x00 - 0xFF Success! Dump of configuration register, 1 bit is equal to 1 of the physical pins (Lower 8 bits of 16 bits result) - * - TCA9534_ERROR(-1) Error! Something went wrong in the process of reading the io expander - */ -int16_t get_all_io_pin_direction(TCA9534_IO_EXP* io_exp); - -/** - * @brief Get the current polarity inversion state of all the io expander pins - * - * @param io_exp The io expander instance to read from or write to - * - * @return - * - 0x00 - 0xFF Success! Dump of configuration register, 1 bit is equal to 1 of the physical pins (Lower 8 bits of 16 bits result) - * - TCA9534_ERROR(-1) Error! Something went wrong in the process of reading the io expander - */ -int16_t get_all_io_polarity_inversion(TCA9534_IO_EXP* io_exp); - -/** - * @brief Get the current direction of the specified io expander pin - * - * @param io_exp The io expander instance to read from or write to - * @param io_pin The io expander pin to read polarity inversion from - * - * @return - * - 0 Success! Pin is Not inverted - * - 1 Success! Pin is Inverted - * - TCA9534_ERROR(-1) Error! Something went wrong in the process of reading the io expander - */ -int16_t get_io_pin_polarity_inversion(TCA9534_IO_EXP* io_exp, TCA9534_PINS io_pin); - -/** - * @brief Get the current direction of the specified physical pin (0 means OUTPUT or 1 means INPUT) - * - * @param io_exp The io expander instance to read from or write to - * @param io_pin The io expander pin to read the state from - * - * @return - * - 0 Success! Pin is OUTPUT - * - 1 Success! Pin is INPUT - * - TCA9534_ERROR(-1) Error! Something went wrong in the process of reading the io expander - */ -int16_t get_io_pin_direction(TCA9534_IO_EXP* io_exp, TCA9534_PINS io_pin); - -/** - * @brief Sets all physical pins of the io expander to a specified direction (INPUT or OUTPUT) - * - * @param io_exp The io expander instance to read from or write to - * @param properties The pin direction to be set (INPUT or OUTPUT) - * - * @return - * - ESP_OK Success! - * - ESP_ERR Error! - */ -esp_err_t set_all_tca9534_io_pins_direction(TCA9534_IO_EXP* io_exp, TCA9534_PORT_DIRECTION properties); - -/** - * @brief Set physical pin of the io expander to a specified direction (INPUT or OUTPUT) - * - * @param io_exp The io expander instance to read from or write to - * @param io_pin The io expander physical pin to be set - * @param properties The pin direction to be set (INPUT or OUTPUT) - * - * @return - * - ESP_OK Success! - * - ESP_ERR Error! - */ -esp_err_t set_tca9534_io_pin_direction(TCA9534_IO_EXP* io_exp, TCA9534_PINS io_pin, TCA9534_PORT_DIRECTION properties); - -/** - * @brief Set physical pin of the io expander to an specified output state (HIGH(1) or LOW(0)) - * - * @param io_exp The io expander instance to read from or write to - * @param io_pin The io expander physical pin to be set - * @param state The pin state to be set (1 or 0) - * - * @return - * - ESP_OK Success! - * - ESP_ERR Error! - * - * @note Pin output state can be inverted with the inversion register - */ -esp_err_t set_tca9534_io_pin_output_state(TCA9534_IO_EXP* io_exp, TCA9534_PINS io_pin, uint8_t state); - -#ifdef __cplusplus -} -#endif - -#endif //TCA9534_IDF_TCA9534_H diff --git a/Drivers/TCA9534/src/TCA9534.c b/Drivers/TCA9534/src/TCA9534.c deleted file mode 100644 index 9fb6ad08c..000000000 --- a/Drivers/TCA9534/src/TCA9534.c +++ /dev/null @@ -1,151 +0,0 @@ -#include "TCA9534.h" -#include "driver/i2c.h" -#include "esp_log.h" -#include - -#define I2C_MASTER_TIMEOUT_MS 1000 - -#define TCA9534_LIB_TAG "TCA9534" -#define TCA9534_IO_NUM 8 - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @brief TCA9534 Internal configuration and pin registers - */ -typedef enum { - TCA9534_REG_INPUT_PORT, - TCA9534_REG_OUTPUT_PORT, - TCA9534_REG_POLARITY_INVERSION, - TCA9534_REG_CONFIGURATION -} TCA9534_REGISTER; - -/** - * @brief Default TCA9534 interrupt task - */ -void TCA9534_default_interrupt_task(void * pvParameters){ - ESP_LOGW(TCA9534_LIB_TAG, "No interrupt task defined! Using standard TCA9523 interrupt task!"); - TCA9534_IO_EXP* io_exp = (TCA9534_IO_EXP*) pvParameters; - uint32_t io_num; - while(1){ - if(xTaskNotifyWait(0,0,&io_num,portTICK_PERIOD_MS) == pdTRUE) { - uint8_t input_status = get_tca9534_all_io_pin_input_status(io_exp); - printf("Current input status (pin : status):\n"); - for (uint8_t i = 0; i < TCA9534_IO_NUM; i++) - printf("P%d : %d\n", i, (input_status & (1<interrupt_task, 0, eNoAction, 0); -} - -/** -* @brief Setup TCA9534 interrupts -*/ -void setup_tca9534_interrupt_handler(TCA9534_IO_EXP* io_exp){ - if(io_exp->interrupt_task == NULL){ - xTaskCreate( - TCA9534_default_interrupt_task, /* Function that implements the task. */ - "NAME", /* Text name for the task. */ - 2048, /* Stack size in words, not bytes. */ - ( void * ) io_exp, /* Parameter passed into the task. */ - 10,/* Priority at which the task is created. */ - io_exp->interrupt_task); /* Used to pass out the created task's handle. */ - - } - - gpio_pad_select_gpio(GPIO_NUM_26); - gpio_set_direction(GPIO_NUM_26,GPIO_MODE_INPUT); - gpio_intr_enable(GPIO_NUM_26); - - gpio_set_intr_type(io_exp->interrupt_pin, GPIO_INTR_NEGEDGE); - gpio_install_isr_service(0); - gpio_isr_handler_add(io_exp->interrupt_pin, TCA9534_interrupt_handler, (void *)io_exp); -} - -esp_err_t write_tca9534_reg(TCA9534_IO_EXP* io_exp, TCA9534_REGISTER cmd, uint8_t data) { - uint8_t write_buffer[2] = {cmd, data}; - return i2c_master_write_to_device(io_exp->i2c_master_port, io_exp->I2C_ADDR, write_buffer, - sizeof(write_buffer), I2C_MASTER_TIMEOUT_MS / portTICK_PERIOD_MS); -} - -esp_err_t read_tca9534_reg(TCA9534_IO_EXP* io_exp, TCA9534_REGISTER cmd, uint8_t *read_buff) { - uint8_t reg = cmd; - return i2c_master_write_read_device(io_exp->i2c_master_port, io_exp->I2C_ADDR, ®, - 1, read_buff, 1, I2C_MASTER_TIMEOUT_MS / portTICK_PERIOD_MS); -} - -int16_t get_tca9534_all_io_pin_input_status(TCA9534_IO_EXP* io_exp) { - uint8_t result = 0; - esp_err_t status = read_tca9534_reg(io_exp, TCA9534_REG_INPUT_PORT, &result); - return (status == ESP_OK) ? result : TCA9534_ERROR; -} - -int16_t get_io_pin_input_status(TCA9534_IO_EXP* io_exp, TCA9534_PINS io_pin) { - int16_t result = get_tca9534_all_io_pin_input_status(io_exp); - if (result != TCA9534_ERROR) - result &= (1 << io_pin); - return (result == (1<< io_pin)); -} - -int16_t get_all_io_pin_direction(TCA9534_IO_EXP* io_exp) { - uint8_t result; - esp_err_t status = read_tca9534_reg(io_exp, TCA9534_REG_CONFIGURATION, &result); - return (status == ESP_OK) ? result : TCA9534_ERROR; -} - -int16_t get_io_pin_direction(TCA9534_IO_EXP* io_exp, TCA9534_PINS io_pin) { - int16_t result = get_all_io_pin_direction(io_exp); - if (result != TCA9534_ERROR) - result &= (1 << io_pin); - return (result == (1<< io_pin)); -} - -int16_t get_all_io_polarity_inversion(TCA9534_IO_EXP* io_exp) { - uint8_t result; - esp_err_t status = read_tca9534_reg(io_exp, TCA9534_REG_POLARITY_INVERSION, &result); - return (status == ESP_OK) ? result : TCA9534_ERROR; -} - -int16_t get_io_pin_polarity_inversion(TCA9534_IO_EXP* io_exp, TCA9534_PINS io_pin) { - int16_t result = get_all_io_polarity_inversion(io_exp); - if (result != TCA9534_ERROR) - result &= (1 << io_pin); - return (result == (1<< io_pin)); -} - -esp_err_t set_all_tca9534_io_pins_direction(TCA9534_IO_EXP* io_exp, TCA9534_PORT_DIRECTION properties) { - uint8_t dir = (properties == TCA9534_OUTPUT) ? 0x00 : 0xFF; - esp_err_t status = write_tca9534_reg(io_exp, TCA9534_REG_CONFIGURATION, dir); - return status; -} - -esp_err_t set_tca9534_io_pin_direction(TCA9534_IO_EXP* io_exp, TCA9534_PINS io_pin, TCA9534_PORT_DIRECTION properties) { - uint8_t port_status = 0; - esp_err_t status = read_tca9534_reg(io_exp, TCA9534_REG_CONFIGURATION, &port_status); - port_status = (properties != TCA9534_OUTPUT) ? (port_status | (1 << io_pin)) : (port_status & ~(1 << io_pin)); - - status |= write_tca9534_reg(io_exp, TCA9534_REG_CONFIGURATION, port_status); - return status; -} - -esp_err_t set_tca9534_io_pin_output_state(TCA9534_IO_EXP* io_exp, TCA9534_PINS io_pin, uint8_t state) { - uint8_t port_status = 0; - esp_err_t status = read_tca9534_reg(io_exp, TCA9534_REG_OUTPUT_PORT, &port_status); - port_status = (state != 0) ? (port_status | (1 << io_pin)) : (port_status & ~(1 << io_pin)); - - status |= write_tca9534_reg(io_exp, TCA9534_REG_OUTPUT_PORT, port_status); - return status; -} - -#ifdef __cplusplus -} -#endif diff --git a/Drivers/axs15231b-module/CMakeLists.txt b/Drivers/axs15231b-module/CMakeLists.txt new file mode 100644 index 000000000..74f22b263 --- /dev/null +++ b/Drivers/axs15231b-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(axs15231b-module + SRCS ${SOURCE_FILES} + INCLUDE_DIRS include/ + REQUIRES TactilityKernel platform-esp32 esp_lcd_axs15231b driver +) diff --git a/Drivers/axs15231b-module/LICENSE-Apache-2.0.md b/Drivers/axs15231b-module/LICENSE-Apache-2.0.md new file mode 100644 index 000000000..f5f4b8b5e --- /dev/null +++ b/Drivers/axs15231b-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/axs15231b-module/bindings/axs,axs15231b-touch.yaml b/Drivers/axs15231b-module/bindings/axs,axs15231b-touch.yaml new file mode 100644 index 000000000..818eb6cc5 --- /dev/null +++ b/Drivers/axs15231b-module/bindings/axs,axs15231b-touch.yaml @@ -0,0 +1,45 @@ +description: AXS15231B touch controller (I2C side of the combined display+touch chip) + +include: ["i2c-device.yaml"] + +compatible: "axs,axs15231b-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 + 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/axs15231b-module/bindings/axs,axs15231b.yaml b/Drivers/axs15231b-module/bindings/axs,axs15231b.yaml new file mode 100644 index 000000000..0ee27f5ca --- /dev/null +++ b/Drivers/axs15231b-module/bindings/axs,axs15231b.yaml @@ -0,0 +1,67 @@ +description: > + AXS15231B display panel (QSPI interface). Combined display+touch controller chip - see + axs,axs15231b-touch for the touch side, which sits on a separate I2C bus and is modeled as an + independent devicetree node. + +compatible: "axs,axs15231b" + +bus: spi + +properties: + horizontal-resolution: + type: int + required: true + description: Horizontal resolution in pixels + vertical-resolution: + type: int + required: true + description: Vertical resolution in pixels + 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: QSPI pixel clock frequency in Hz + transaction-queue-depth: + type: int + default: 10 + description: Size of the internal SPI transaction queue + 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 + pin-te: + type: phandles + default: GPIO_PIN_SPEC_NONE + description: Optional Tearing-Effect GPIO pin. When set, draw_bitmap waits (best-effort, up + to 20ms) for a V-blank pulse on this pin before starting each transfer, to reduce visible + tearing. Omit to skip TE sync entirely. + 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. Omit to use the + AXS15231B 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/axs15231b-module/devicetree.yaml b/Drivers/axs15231b-module/devicetree.yaml new file mode 100644 index 000000000..a07d6f334 --- /dev/null +++ b/Drivers/axs15231b-module/devicetree.yaml @@ -0,0 +1,3 @@ +dependencies: + - TactilityKernel +bindings: bindings diff --git a/Drivers/axs15231b-module/include/axs15231b_module.h b/Drivers/axs15231b-module/include/axs15231b_module.h new file mode 100644 index 000000000..09f7591c7 --- /dev/null +++ b/Drivers/axs15231b-module/include/axs15231b_module.h @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +extern struct Module axs15231b_module; + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/axs15231b-module/include/bindings/axs15231b_display.h b/Drivers/axs15231b-module/include/bindings/axs15231b_display.h new file mode 100644 index 000000000..5f2e73637 --- /dev/null +++ b/Drivers/axs15231b-module/include/bindings/axs15231b_display.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. "axs,axs15231b" -> axs15231b_config_dt), not from the node name or +// driver name, so the tag here must match that exactly. +DEFINE_DEVICETREE(axs15231b, struct Axs15231bDisplayConfig) diff --git a/Drivers/axs15231b-module/include/bindings/axs15231b_touch.h b/Drivers/axs15231b-module/include/bindings/axs15231b_touch.h new file mode 100644 index 000000000..8455f5cba --- /dev/null +++ b/Drivers/axs15231b-module/include/bindings/axs15231b_touch.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. "axs,axs15231b-touch" -> axs15231b_touch_config_dt), not from the node +// name or driver name, so the tag here must match that exactly. +DEFINE_DEVICETREE(axs15231b_touch, struct Axs15231bTouchConfig) diff --git a/Drivers/axs15231b-module/include/drivers/axs15231b_display.h b/Drivers/axs15231b-module/include/drivers/axs15231b_display.h new file mode 100644 index 000000000..77f353083 --- /dev/null +++ b/Drivers/axs15231b-module/include/drivers/axs15231b_display.h @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include +#include + +struct Axs15231bDisplayConfig { + uint16_t horizontal_resolution; + uint16_t vertical_resolution; + bool mirror_x; + bool mirror_y; + bool invert_color; + bool bgr_order; + uint32_t pixel_clock_hz; + uint32_t transaction_queue_depth; + + // Reset pin. GPIO_PIN_SPEC_NONE means no reset line is wired up (matches the original + // deprecated-HAL config for the boards using this chip so far). + struct GpioPinSpec pin_reset; + bool reset_active_high; + + // Optional Tearing-Effect GPIO pin. When set (not GPIO_PIN_SPEC_NONE), draw_bitmap() waits + // (best-effort, up to 20ms) for a V-blank pulse on this pin before starting each transfer, to + // reduce visible tearing. GPIO_PIN_SPEC_NONE skips TE sync entirely. + struct GpioPinSpec pin_te; + + // 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 + // AXS15231B component's own built-in default sequence. + 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/axs15231b-module/include/drivers/axs15231b_touch.h b/Drivers/axs15231b-module/include/drivers/axs15231b_touch.h new file mode 100644 index 000000000..47c041dba --- /dev/null +++ b/Drivers/axs15231b-module/include/drivers/axs15231b_touch.h @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include +#include + +struct Axs15231bTouchConfig { + // Devicetree address hint. Unused by the driver: the AXS15231B always sits at a fixed + // I2C address (see ESP_LCD_TOUCH_IO_I2C_AXS15231B_ADDRESS in esp_lcd_axs15231b.h). + 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/axs15231b-module/source/axs15231b_display.cpp b/Drivers/axs15231b-module/source/axs15231b_display.cpp new file mode 100644 index 000000000..76830668f --- /dev/null +++ b/Drivers/axs15231b-module/source/axs15231b_display.cpp @@ -0,0 +1,478 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include + +#include + +#define TAG "AXS15231B" +#define GET_CONFIG(device) (static_cast((device)->config)) + +struct Axs15231bDisplayInternal { + esp_lcd_panel_io_handle_t io_handle; + esp_lcd_panel_handle_t panel_handle; + // Given from ISR context by on_color_trans_done() once a queued transfer physically + // completes. draw_bitmap() blocks on this so it can honor DisplayApi's synchronous contract + // (see lvgl_display.c: the caller reuses/overwrites the color buffer as soon as draw_bitmap + // returns) - esp_lcd_panel_draw_bitmap() itself only queues the transfer and returns early. + SemaphoreHandle_t draw_done_semaphore; + // Non-null only when a TE (Tearing-Effect) pin is configured. Signaled by te_isr_handler() + // on each rising edge, so draw_bitmap() can wait for the next V-blank before transferring. + SemaphoreHandle_t te_semaphore; + bool te_isr_installed; + // Whether we're the one who called gpio_install_isr_service() - if so, we must be the one to + // uninstall it, but only if no other pin on the system is still relying on it. + bool te_isr_service_installed_by_us; + axs15231b_lcd_init_cmd_t* parsed_init_cmds; +}; + +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 void IRAM_ATTR te_isr_handler(void* arg) { + auto* semaphore = static_cast(arg); + BaseType_t high_task_woken = pdFALSE; + xSemaphoreGiveFromISR(semaphore, &high_task_woken); + if (high_task_woken == pdTRUE) { + portYIELD_FROM_ISR(); + } +} + +static int pin_or_unused(const struct GpioPinSpec& pin) { + return pin.gpio_controller == nullptr ? -1 : static_cast(pin.pin); +} + +static gpio_num_t pin_or_nc(const struct GpioPinSpec& pin) { + return pin.gpio_controller == nullptr ? GPIO_NUM_NC : static_cast(pin.pin); +} + +// 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/axs,axs15231b.yaml) into a heap-allocated axs15231b_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, axs15231b_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(axs15231b_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 + +// Best-effort: a TE pin is a hardware refinement, not a requirement, so failures here are logged +// and left for the caller to treat as non-fatal (matches the original deprecated-HAL driver, +// which continued without TE sync if setup failed). +static bool setup_te_sync(Axs15231bDisplayInternal* internal, gpio_num_t te_pin) { + if (te_pin == GPIO_NUM_NC) { + return true; + } + + internal->te_semaphore = xSemaphoreCreateBinary(); + if (internal->te_semaphore == nullptr) { + LOG_E(TAG, "Failed to create TE sync semaphore"); + return false; + } + + gpio_config_t io_conf = { + .pin_bit_mask = 1ULL << te_pin, + .mode = GPIO_MODE_INPUT, + .pull_up_en = GPIO_PULLUP_DISABLE, + .pull_down_en = GPIO_PULLDOWN_DISABLE, + .intr_type = GPIO_INTR_POSEDGE, + }; + if (gpio_config(&io_conf) != ESP_OK) { + LOG_E(TAG, "Failed to configure TE GPIO"); + vSemaphoreDelete(internal->te_semaphore); + internal->te_semaphore = nullptr; + return false; + } + + esp_err_t ret = gpio_install_isr_service(ESP_INTR_FLAG_IRAM); + if (ret == ESP_OK) { + internal->te_isr_service_installed_by_us = true; + } else if (ret != ESP_ERR_INVALID_STATE) { // ESP_ERR_INVALID_STATE means it's already installed elsewhere + LOG_E(TAG, "Failed to install GPIO ISR service"); + vSemaphoreDelete(internal->te_semaphore); + internal->te_semaphore = nullptr; + return false; + } + + if (gpio_isr_handler_add(te_pin, te_isr_handler, internal->te_semaphore) != ESP_OK) { + LOG_E(TAG, "Failed to add TE ISR handler"); + if (internal->te_isr_service_installed_by_us) { + gpio_uninstall_isr_service(); + internal->te_isr_service_installed_by_us = false; + } + vSemaphoreDelete(internal->te_semaphore); + internal->te_semaphore = nullptr; + return false; + } + + internal->te_isr_installed = true; + return true; +} + +static void teardown_te_sync(Axs15231bDisplayInternal* internal, gpio_num_t te_pin) { + if (internal->te_isr_installed) { + gpio_isr_handler_remove(te_pin); + gpio_intr_disable(te_pin); + internal->te_isr_installed = false; + } + if (internal->te_isr_service_installed_by_us) { + gpio_uninstall_isr_service(); + internal->te_isr_service_installed_by_us = false; + } + if (internal->te_semaphore != nullptr) { + vSemaphoreDelete(internal->te_semaphore); + internal->te_semaphore = nullptr; + } +} + +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(Axs15231bDisplayInternal))); + if (internal == nullptr) { + return ERROR_OUT_OF_MEMORY; + } + internal->te_semaphore = nullptr; + internal->te_isr_installed = false; + internal->te_isr_service_installed_by_us = false; + internal->parsed_init_cmds = nullptr; + + const axs15231b_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; + } + + internal->draw_done_semaphore = xSemaphoreCreateBinary(); + if (internal->draw_done_semaphore == nullptr) { + free(internal->parsed_init_cmds); + free(internal); + return ERROR_OUT_OF_MEMORY; + } + + // AXS15231B is only ever driven over QSPI in this codebase (no plain-SPI/i80 board has shown + // up yet), so quad_mode/lcd_cmd_bits/lcd_param_bits are fixed rather than devicetree knobs - + // matches AXS15231B_PANEL_IO_QSPI_CONFIG() in esp_lcd_axs15231b.h. dc_gpio_num is unused in + // QSPI mode (command/data framing goes over the command byte instead of a DC line). + esp_lcd_panel_io_spi_config_t io_config = { + .cs_gpio_num = pin_or_unused(cs_pin), + .dc_gpio_num = -1, + .spi_mode = 3, + .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 = 32, + .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 = 1, + .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->parsed_init_cmds); + free(internal); + return ERROR_RESOURCE; + } + + axs15231b_vendor_config_t vendor_config = { + .init_cmds = init_cmds, + .init_cmds_size = init_cmds_size, + .flags = { + .use_qspi_interface = 1, + }, + }; + + 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_axs15231b(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->parsed_init_cmds); + free(internal); + return ERROR_RESOURCE; + } + + // Bring-up sequence. swap_xy is intentionally not called (and not exposed in DisplayApi + // below): it doesn't work on this chip/panel combination (confirmed on real hardware by the + // original deprecated-HAL driver). Every failure path below 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_mirror(internal->panel_handle, config->mirror_x, config->mirror_y) == ESP_OK && + esp_lcd_panel_invert_color(internal->panel_handle, config->invert_color) == 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); + vSemaphoreDelete(internal->draw_done_semaphore); + free(internal->parsed_init_cmds); + free(internal); + return ERROR_RESOURCE; + } + + if (!setup_te_sync(internal, pin_or_nc(config->pin_te))) { + LOG_W(TAG, "TE sync setup failed, continuing without TE synchronization"); + } + + device_set_driver_data(device, internal); + return ERROR_NONE; +} + +static error_t stop(Device* device) { + const auto* config = GET_CONFIG(device); + auto* internal = static_cast(device_get_driver_data(device)); + + teardown_te_sync(internal, pin_or_nc(config->pin_te)); + + 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->parsed_init_cmds); + free(internal); + device_set_driver_data(device, nullptr); + return ERROR_NONE; +} + +// endregion + +// region DisplayApi + +static error_t axs15231b_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 axs15231b_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 axs15231b_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)); + + // Best-effort wait for the next V-blank pulse before transferring, to reduce visible tearing. + // Non-fatal if it times out (matches the original deprecated-HAL driver) - draw_bitmap() + // still has to happen even if TE sync missed its window. + if (internal->te_semaphore != nullptr) { + xSemaphoreTake(internal->te_semaphore, 0); // drain any already-pending signal + xSemaphoreTake(internal->te_semaphore, pdMS_TO_TICKS(20)); + } + + // Drain any stale signal left over from a prior non-draw transaction (bring-up commands like + // reset/init also complete through on_color_trans_done), so the take() below can only be + // satisfied by this draw's own transfer completing. + 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 axs15231b_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 axs15231b_get_mirror_x(Device* device) { + return GET_CONFIG(device)->mirror_x; +} + +static bool axs15231b_get_mirror_y(Device* device) { + return GET_CONFIG(device)->mirror_y; +} + +// swap_xy/set_gap/disp_sleep are not exposed: swap_xy is confirmed non-functional on this +// chip/panel combination on real hardware (see start()'s comment), and the AXS15231B component +// doesn't implement set_gap or disp_sleep at all. + +static error_t axs15231b_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 axs15231b_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; +} + +// _SWAPPED (not plain RGB565): the panel expects each 16-bit pixel high-byte-first over the QSPI +// bus, but this CPU is little-endian - the original deprecated-HAL driver did the equivalent +// byte-swap by hand in its custom flush callback (lv_draw_sw_rgb565_swap()); the generic +// lvgl-module bridge does it for us once we report this format (see lvgl_display_map_color_format()). +static enum DisplayColorFormat axs15231b_get_color_format(Device*) { + return DISPLAY_COLOR_FORMAT_RGB565_SWAPPED; +} + +static uint16_t axs15231b_get_resolution_x(Device* device) { + return GET_CONFIG(device)->horizontal_resolution; +} + +static uint16_t axs15231b_get_resolution_y(Device* device) { + return GET_CONFIG(device)->vertical_resolution; +} + +static void axs15231b_get_frame_buffer(Device*, uint8_t, void** out_buffer) { + *out_buffer = nullptr; +} + +static uint8_t axs15231b_get_frame_buffer_count(Device*) { + return 0; +} + +static error_t axs15231b_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 axs15231b_display_api = { + .capabilities = DISPLAY_CAPABILITY_CAP_MIRROR | DISPLAY_CAPABILITY_INVERT_COLOR | + DISPLAY_CAPABILITY_ON_OFF | DISPLAY_CAPABILITY_BACKLIGHT, + .reset = axs15231b_reset, + .init = axs15231b_init, + .draw_bitmap = axs15231b_draw_bitmap, + .mirror = axs15231b_mirror, + .swap_xy = nullptr, + .get_swap_xy = nullptr, + .get_mirror_x = axs15231b_get_mirror_x, + .get_mirror_y = axs15231b_get_mirror_y, + .set_gap = nullptr, + .invert_color = axs15231b_invert_color, + .disp_on_off = axs15231b_disp_on_off, + .disp_sleep = nullptr, + .get_color_format = axs15231b_get_color_format, + .get_resolution_x = axs15231b_get_resolution_x, + .get_resolution_y = axs15231b_get_resolution_y, + .get_frame_buffer = axs15231b_get_frame_buffer, + .get_frame_buffer_count = axs15231b_get_frame_buffer_count, + .get_backlight = axs15231b_get_backlight, + .has_capability = nullptr, +}; + +Driver axs15231b_display_driver = { + .name = "axs15231b_display", + .compatible = (const char*[]) { "axs,axs15231b", nullptr }, + .start_device = start, + .stop_device = stop, + .api = &axs15231b_display_api, + .device_type = &DISPLAY_TYPE, + .owner = &axs15231b_module, + .internal = nullptr +}; diff --git a/Drivers/axs15231b-module/source/axs15231b_touch.cpp b/Drivers/axs15231b-module/source/axs15231b_touch.cpp new file mode 100644 index 000000000..5c6dbd413 --- /dev/null +++ b/Drivers/axs15231b-module/source/axs15231b_touch.cpp @@ -0,0 +1,211 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +#define TAG "AXS15231BTouch" +#define GET_CONFIG(device) (static_cast((device)->config)) + +struct Axs15231bTouchInternal { + esp_lcd_panel_io_handle_t io_handle; + esp_lcd_touch_handle_t touch_handle; +}; + +static inline gpio_num_t pin_or_nc(const struct GpioPinSpec& pin) { + return pin.gpio_controller == nullptr ? GPIO_NUM_NC : static_cast(pin.pin); +} + +// region Driver lifecycle + +// AXS15231B always sits at a fixed I2C address (ESP_LCD_TOUCH_IO_I2C_AXS15231B_ADDRESS), unlike +// GT911's strapping-dependent address, so no bus probing is needed 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_AXS15231B_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(Axs15231bTouchInternal))); + 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_axs15231b(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 axs15231b_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 axs15231b_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 axs15231b_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 axs15231b_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 axs15231b_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 axs15231b_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 axs15231b_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 axs15231b_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 axs15231b_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 axs15231b_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 axs15231b_touch_pointer_api = { + .enter_sleep = axs15231b_touch_enter_sleep, + .exit_sleep = axs15231b_touch_exit_sleep, + .read_data = axs15231b_touch_read_data, + .get_touched_points = axs15231b_touch_get_touched_points, + .set_swap_xy = axs15231b_touch_set_swap_xy, + .get_swap_xy = axs15231b_touch_get_swap_xy, + .set_mirror_x = axs15231b_touch_set_mirror_x, + .get_mirror_x = axs15231b_touch_get_mirror_x, + .set_mirror_y = axs15231b_touch_set_mirror_y, + .get_mirror_y = axs15231b_touch_get_mirror_y, +}; + +Driver axs15231b_touch_driver = { + .name = "axs15231b_touch", + .compatible = (const char*[]) { "axs,axs15231b-touch", nullptr }, + .start_device = start, + .stop_device = stop, + .api = &axs15231b_touch_pointer_api, + .device_type = &POINTER_TYPE, + .owner = &axs15231b_module, + .internal = nullptr +}; diff --git a/Drivers/axs15231b-module/source/module.cpp b/Drivers/axs15231b-module/source/module.cpp new file mode 100644 index 000000000..600e96baa --- /dev/null +++ b/Drivers/axs15231b-module/source/module.cpp @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include + +extern "C" { + +extern Driver axs15231b_display_driver; +extern Driver axs15231b_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(&axs15231b_display_driver) == ERROR_NONE); + check(driver_construct_add(&axs15231b_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(&axs15231b_touch_driver) == ERROR_NONE); + check(driver_remove_destruct(&axs15231b_display_driver) == ERROR_NONE); + return ERROR_NONE; +} + +Module axs15231b_module = { + .name = "axs15231b", + .start = start, + .stop = stop, + .symbols = nullptr, + .internal = nullptr +}; + +} // extern "C" diff --git a/Drivers/bq25896-module/CMakeLists.txt b/Drivers/bq25896-module/CMakeLists.txt new file mode 100644 index 000000000..2b602f94c --- /dev/null +++ b/Drivers/bq25896-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(bq25896-module + SRCS ${SOURCE_FILES} + INCLUDE_DIRS include/ + REQUIRES TactilityKernel +) diff --git a/Drivers/bq25896-module/bindings/ti,bq25896.yaml b/Drivers/bq25896-module/bindings/ti,bq25896.yaml new file mode 100644 index 000000000..9099a8ccc --- /dev/null +++ b/Drivers/bq25896-module/bindings/ti,bq25896.yaml @@ -0,0 +1,11 @@ +description: > + TI BQ25896 1-cell 3A buck battery charger with power path and PSEL. Creates a "power-supply" + child device (see drivers/bq25896.h) exposing ship-mode power-off only - matches the + deprecated HAL's Bq25896, which never read charging status/metrics from this chip (that came + from a separate fuel gauge, if present - see the bq27220 driver). + +include: ["i2c-device.yaml"] + +compatible: "ti,bq25896" + +bus: i2c diff --git a/Drivers/bq25896-module/devicetree.yaml b/Drivers/bq25896-module/devicetree.yaml new file mode 100644 index 000000000..a07d6f334 --- /dev/null +++ b/Drivers/bq25896-module/devicetree.yaml @@ -0,0 +1,3 @@ +dependencies: + - TactilityKernel +bindings: bindings diff --git a/Drivers/bq25896-module/include/bindings/bq25896.h b/Drivers/bq25896-module/include/bindings/bq25896.h new file mode 100644 index 000000000..a18a513b5 --- /dev/null +++ b/Drivers/bq25896-module/include/bindings/bq25896.h @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include + +DEFINE_DEVICETREE(bq25896, struct Bq25896Config) diff --git a/Drivers/bq25896-module/include/bq25896_module.h b/Drivers/bq25896-module/include/bq25896_module.h new file mode 100644 index 000000000..f3a368956 --- /dev/null +++ b/Drivers/bq25896-module/include/bq25896_module.h @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +extern struct Module bq25896_module; + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/bq25896-module/include/drivers/bq25896.h b/Drivers/bq25896-module/include/drivers/bq25896.h new file mode 100644 index 000000000..eec1e7e61 --- /dev/null +++ b/Drivers/bq25896-module/include/drivers/bq25896.h @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +struct Bq25896Config { + // Devicetree address hint (0x6B). + uint8_t address; +}; + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/bq25896-module/source/bq25896.cpp b/Drivers/bq25896-module/source/bq25896.cpp new file mode 100644 index 000000000..a28dc5303 --- /dev/null +++ b/Drivers/bq25896-module/source/bq25896.cpp @@ -0,0 +1,175 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include + +#include +#include +#include +#include +#include +#include + +#include + +#define TAG "BQ25896" +#define GET_CONFIG(device) (static_cast((device)->config)) +// The power-supply child's config pointer isn't set; it reads its settings from its parent +// (the bq25896 device) instead - same convention as the sy6970/bq27220 drivers. +#define GET_PARENT_CONFIG(device) (static_cast(device_get_parent(device)->config)) +#define GET_I2C_BUS(device) (device_get_parent(device_get_parent(device))) + +static const TickType_t I2C_TIMEOUT = pdMS_TO_TICKS(50); + +// REG09 bit5: BATFET_DIS. 0 = BATFET on (normal operation), 1 = BATFET off (ship mode). +// Register offset is shared with the SY6970 (see that driver's own comment). +static constexpr uint8_t REG_09 = 0x09; +static constexpr uint8_t REG09_BATFET_DIS = 1u << 5; + +extern "C" { + +// region PowerSupplyApi (power-supply child device) + +// Matches the deprecated HAL's Bq25896: no charging status/metrics were ever read from this +// chip (those came from a separate fuel gauge, if present - see the bq27220 driver), so the +// only capability exposed here is ship-mode power-off. +static bool supports_property(Device*, PowerSupplyProperty) { return false; } +static error_t get_property(Device*, PowerSupplyProperty, PowerSupplyPropertyValue*) { return ERROR_NOT_SUPPORTED; } +static bool supports_charge_control(Device*) { return false; } +static bool is_allowed_to_charge(Device*) { return false; } +static error_t set_allowed_to_charge(Device*, bool) { return ERROR_NOT_SUPPORTED; } +static bool supports_quick_charge(Device*) { return false; } +static bool is_quick_charge_enabled(Device*) { return false; } +static error_t set_quick_charge_enabled(Device*, bool) { return ERROR_NOT_SUPPORTED; } + +static bool supports_power_off(Device*) { return true; } + +static error_t power_off(Device* device) { + auto* i2c = GET_I2C_BUS(device); + const auto* config = GET_PARENT_CONFIG(device); + LOG_I(TAG, "Power off (BATFET_DIS)"); + return i2c_controller_register8_set_bits(i2c, config->address, REG_09, REG09_BATFET_DIS, I2C_TIMEOUT); +} + +static constexpr PowerSupplyApi BQ25896_POWER_SUPPLY_API = { + .supports_property = supports_property, + .get_property = get_property, + .supports_charge_control = supports_charge_control, + .is_allowed_to_charge = is_allowed_to_charge, + .set_allowed_to_charge = set_allowed_to_charge, + .supports_quick_charge = supports_quick_charge, + .is_quick_charge_enabled = is_quick_charge_enabled, + .set_quick_charge_enabled = set_quick_charge_enabled, + .supports_power_off = supports_power_off, + .power_off = power_off, +}; + +// Registered (driver_construct_add() in module.cpp) so driver_bind() has a valid ->internal, but +// never matched against a devicetree node: bq25896_driver wires it up directly by pointer. +Driver bq25896_power_supply_driver = { + .name = "bq25896-power-supply", + .compatible = (const char*[]) { "bq25896-power-supply", nullptr }, + .start_device = nullptr, + .stop_device = nullptr, + .api = &BQ25896_POWER_SUPPLY_API, + .device_type = &POWER_SUPPLY_TYPE, + .owner = &bq25896_module, + .internal = nullptr +}; + +// endregion + +// region Driver lifecycle (bq25896 device itself) + +struct Bq25896Internal { + 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 = "bq25896-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, &bq25896_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; +} + +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); + + // Matches the deprecated HAL's Bq25896::powerOn(): ensure the BATFET is on (normal + // operation), in case a previous run left it in ship mode. + LOG_I(TAG, "Power on"); + i2c_controller_register8_reset_bits(parent, config->address, REG_09, REG09_BATFET_DIS, I2C_TIMEOUT); + + auto* internal = new (std::nothrow) Bq25896Internal(); + 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); + 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; +} + +Driver bq25896_driver = { + .name = "bq25896", + .compatible = (const char*[]) { "ti,bq25896", nullptr }, + .start_device = start, + .stop_device = stop, + .api = nullptr, + .device_type = nullptr, + .owner = &bq25896_module, + .internal = nullptr +}; + +// endregion + +} diff --git a/Drivers/bq25896-module/source/module.cpp b/Drivers/bq25896-module/source/module.cpp new file mode 100644 index 000000000..7aefdf34b --- /dev/null +++ b/Drivers/bq25896-module/source/module.cpp @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include + +extern "C" { + +extern Driver bq25896_driver; +extern Driver bq25896_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(&bq25896_power_supply_driver) == ERROR_NONE); + check(driver_construct_add(&bq25896_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(&bq25896_driver) == ERROR_NONE); + check(driver_remove_destruct(&bq25896_power_supply_driver) == ERROR_NONE); + return ERROR_NONE; +} + +Module bq25896_module = { + .name = "bq25896", + .start = start, + .stop = stop, + .symbols = nullptr, + .internal = nullptr +}; + +} // extern "C" diff --git a/Drivers/bq27220-module/CMakeLists.txt b/Drivers/bq27220-module/CMakeLists.txt new file mode 100644 index 000000000..17d4ba420 --- /dev/null +++ b/Drivers/bq27220-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(bq27220-module + SRCS ${SOURCE_FILES} + INCLUDE_DIRS include/ + REQUIRES TactilityKernel +) diff --git a/Drivers/bq27220-module/bindings/ti,bq27220.yaml b/Drivers/bq27220-module/bindings/ti,bq27220.yaml new file mode 100644 index 000000000..4168d480d --- /dev/null +++ b/Drivers/bq27220-module/bindings/ti,bq27220.yaml @@ -0,0 +1,12 @@ +description: > + Texas Instruments BQ27220 CEDV battery fuel gauge. Creates a "power-supply" child device + (see drivers/bq27220.h) exposing battery current, voltage, charge-level and charging status + (derived from the BatteryStatus register's DSG/discharge flag); charge control and power-off + are not available from this chip alone (they come from a separate charger, if present - see + the sy6970 driver). + +include: ["i2c-device.yaml"] + +compatible: "ti,bq27220" + +bus: i2c diff --git a/Drivers/bq27220-module/devicetree.yaml b/Drivers/bq27220-module/devicetree.yaml new file mode 100644 index 000000000..a07d6f334 --- /dev/null +++ b/Drivers/bq27220-module/devicetree.yaml @@ -0,0 +1,3 @@ +dependencies: + - TactilityKernel +bindings: bindings diff --git a/Drivers/bq27220-module/include/bindings/bq27220.h b/Drivers/bq27220-module/include/bindings/bq27220.h new file mode 100644 index 000000000..c543a80ec --- /dev/null +++ b/Drivers/bq27220-module/include/bindings/bq27220.h @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include + +DEFINE_DEVICETREE(bq27220, struct Bq27220Config) diff --git a/Drivers/bq27220-module/include/bq27220_module.h b/Drivers/bq27220-module/include/bq27220_module.h new file mode 100644 index 000000000..ad48e1810 --- /dev/null +++ b/Drivers/bq27220-module/include/bq27220_module.h @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +extern struct Module bq27220_module; + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/bq27220-module/include/drivers/bq27220.h b/Drivers/bq27220-module/include/drivers/bq27220.h new file mode 100644 index 000000000..45e5ad2a8 --- /dev/null +++ b/Drivers/bq27220-module/include/drivers/bq27220.h @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +struct Bq27220Config { + // Devicetree address hint (0x55 on the LilyGO T-Deck Max). + uint8_t address; +}; + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/bq27220-module/source/bq27220.cpp b/Drivers/bq27220-module/source/bq27220.cpp new file mode 100644 index 000000000..4dc2b42bf --- /dev/null +++ b/Drivers/bq27220-module/source/bq27220.cpp @@ -0,0 +1,214 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include + +#include +#include +#include +#include +#include +#include + +#include + +#define TAG "BQ27220" +#define GET_CONFIG(device) (static_cast((device)->config)) +// The power-supply child's config pointer isn't set; it reads its settings from its parent +// (the bq27220 device) instead - same convention as the sy6970 driver. +#define GET_PARENT_CONFIG(device) (static_cast(device_get_parent(device)->config)) +#define GET_I2C_BUS(device) (device_get_parent(device_get_parent(device))) + +static const TickType_t I2C_TIMEOUT = pdMS_TO_TICKS(50); + +// BQ27220 standard command registers (16-bit, little-endian on the wire). +static constexpr uint8_t BQ27220_REG_VOLTAGE = 0x08; +static constexpr uint8_t BQ27220_REG_BATTERY_STATUS = 0x0A; +static constexpr uint8_t BQ27220_REG_CURRENT = 0x0C; +static constexpr uint8_t BQ27220_REG_STATE_OF_CHARGE = 0x2C; +// BatteryStatus bit 0: DSG (device is in DISCHARGE). Mirrors the deprecated HAL's +// TpagerPower/TdeckmaxPower fallback: "is charging" = not discharging. +static constexpr uint16_t BQ27220_BATTERY_STATUS_DSG = 1u << 0; + +extern "C" { + +// region PowerSupplyApi (power-supply child device) + +static bool supports_property(Device*, PowerSupplyProperty property) { + switch (property) { + case POWER_SUPPLY_PROP_CURRENT: + case POWER_SUPPLY_PROP_VOLTAGE: + case POWER_SUPPLY_PROP_CAPACITY: + case POWER_SUPPLY_PROP_IS_CHARGING: + return true; + default: + return false; + } +} + +static error_t get_property(Device* device, PowerSupplyProperty property, PowerSupplyPropertyValue* out_value) { + auto* i2c = GET_I2C_BUS(device); + const auto* config = GET_PARENT_CONFIG(device); + + switch (property) { + case POWER_SUPPLY_PROP_VOLTAGE: { + uint16_t voltage_mv = 0; + if (i2c_controller_register16le_get(i2c, config->address, BQ27220_REG_VOLTAGE, &voltage_mv, I2C_TIMEOUT) != ERROR_NONE) { + return ERROR_NOT_FOUND; + } + out_value->int_value = voltage_mv; + return ERROR_NONE; + } + case POWER_SUPPLY_PROP_CURRENT: { + uint16_t raw = 0; + if (i2c_controller_register16le_get(i2c, config->address, BQ27220_REG_CURRENT, &raw, I2C_TIMEOUT) != ERROR_NONE) { + return ERROR_NOT_FOUND; + } + out_value->int_value = static_cast(raw); + return ERROR_NONE; + } + case POWER_SUPPLY_PROP_CAPACITY: { + uint16_t soc = 0; + if (i2c_controller_register16le_get(i2c, config->address, BQ27220_REG_STATE_OF_CHARGE, &soc, I2C_TIMEOUT) != ERROR_NONE) { + return ERROR_NOT_FOUND; + } + out_value->int_value = soc; + return ERROR_NONE; + } + case POWER_SUPPLY_PROP_IS_CHARGING: { + uint16_t status = 0; + if (i2c_controller_register16le_get(i2c, config->address, BQ27220_REG_BATTERY_STATUS, &status, I2C_TIMEOUT) != ERROR_NONE) { + return ERROR_NOT_FOUND; + } + out_value->int_value = (status & BQ27220_BATTERY_STATUS_DSG) == 0 ? 1 : 0; + return ERROR_NONE; + } + default: + return ERROR_NOT_SUPPORTED; + } +} + +static bool supports_charge_control(Device*) { return false; } +static bool is_allowed_to_charge(Device*) { return false; } +static error_t set_allowed_to_charge(Device*, bool) { return ERROR_NOT_SUPPORTED; } +static bool supports_quick_charge(Device*) { return false; } +static bool is_quick_charge_enabled(Device*) { return false; } +static error_t set_quick_charge_enabled(Device*, bool) { return ERROR_NOT_SUPPORTED; } +static bool supports_power_off(Device*) { return false; } +static error_t power_off(Device*) { return ERROR_NOT_SUPPORTED; } + +static constexpr PowerSupplyApi BQ27220_POWER_SUPPLY_API = { + .supports_property = supports_property, + .get_property = get_property, + .supports_charge_control = supports_charge_control, + .is_allowed_to_charge = is_allowed_to_charge, + .set_allowed_to_charge = set_allowed_to_charge, + .supports_quick_charge = supports_quick_charge, + .is_quick_charge_enabled = is_quick_charge_enabled, + .set_quick_charge_enabled = set_quick_charge_enabled, + .supports_power_off = supports_power_off, + .power_off = power_off, +}; + +// Registered (driver_construct_add() in module.cpp) so driver_bind() has a valid ->internal, but +// never matched against a devicetree node: bq27220_driver wires it up directly by pointer. +Driver bq27220_power_supply_driver = { + .name = "bq27220-power-supply", + .compatible = (const char*[]) { "bq27220-power-supply", nullptr }, + .start_device = nullptr, + .stop_device = nullptr, + .api = &BQ27220_POWER_SUPPLY_API, + .device_type = &POWER_SUPPLY_TYPE, + .owner = &bq27220_module, + .internal = nullptr +}; + +// endregion + +// region Driver lifecycle (bq27220 device itself) + +struct Bq27220Internal { + 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 = "bq27220-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, &bq27220_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; +} + +static error_t start(Device* device) { + auto* parent = device_get_parent(device); + check(device_get_type(parent) == &I2C_CONTROLLER_TYPE); + + auto* internal = new (std::nothrow) Bq27220Internal(); + 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); + 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; +} + +Driver bq27220_driver = { + .name = "bq27220", + .compatible = (const char*[]) { "ti,bq27220", nullptr }, + .start_device = start, + .stop_device = stop, + .api = nullptr, + .device_type = nullptr, + .owner = &bq27220_module, + .internal = nullptr +}; + +// endregion + +} diff --git a/Drivers/bq27220-module/source/module.cpp b/Drivers/bq27220-module/source/module.cpp new file mode 100644 index 000000000..c287ab2d3 --- /dev/null +++ b/Drivers/bq27220-module/source/module.cpp @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include + +extern "C" { + +extern Driver bq27220_driver; +extern Driver bq27220_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(&bq27220_power_supply_driver) == ERROR_NONE); + check(driver_construct_add(&bq27220_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(&bq27220_driver) == ERROR_NONE); + check(driver_remove_destruct(&bq27220_power_supply_driver) == ERROR_NONE); + return ERROR_NONE; +} + +Module bq27220_module = { + .name = "bq27220", + .start = start, + .stop = stop, + .symbols = nullptr, + .internal = nullptr +}; + +} // extern "C" diff --git a/Drivers/cst328-module/CMakeLists.txt b/Drivers/cst328-module/CMakeLists.txt new file mode 100644 index 000000000..64d8227c3 --- /dev/null +++ b/Drivers/cst328-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(cst328-module + SRCS ${SOURCE_FILES} + INCLUDE_DIRS include/ + REQUIRES TactilityKernel +) diff --git a/Drivers/cst328-module/bindings/hynitron,cst328.yaml b/Drivers/cst328-module/bindings/hynitron,cst328.yaml new file mode 100644 index 000000000..907d125d3 --- /dev/null +++ b/Drivers/cst328-module/bindings/hynitron,cst328.yaml @@ -0,0 +1,32 @@ +description: > + Hynitron CST328 capacitive touch controller (register-compatible with the CST226SE, per + LilyGO's SensorLib TouchClassCST226). Supports up to 5 simultaneous touch points and + self-reports its panel resolution during bring-up. + +include: ["i2c-device.yaml"] + +compatible: "hynitron,cst328" + +bus: i2c + +properties: + swap-xy: + type: boolean + default: false + description: Swap the X and Y axes + mirror-x: + type: boolean + default: false + description: Mirror the X axis (against the chip's own self-reported resolution) + mirror-y: + type: boolean + default: false + description: Mirror the Y axis (against the chip's own self-reported resolution) + pin-reset: + type: phandles + default: GPIO_PIN_SPEC_NONE + description: Optional reset GPIO pin. Falls back to a software reset command if not set. + reset-active-high: + type: boolean + default: false + description: Whether the reset pin is active high diff --git a/Drivers/cst328-module/devicetree.yaml b/Drivers/cst328-module/devicetree.yaml new file mode 100644 index 000000000..a07d6f334 --- /dev/null +++ b/Drivers/cst328-module/devicetree.yaml @@ -0,0 +1,3 @@ +dependencies: + - TactilityKernel +bindings: bindings diff --git a/Drivers/cst328-module/include/bindings/cst328.h b/Drivers/cst328-module/include/bindings/cst328.h new file mode 100644 index 000000000..77a77136b --- /dev/null +++ b/Drivers/cst328-module/include/bindings/cst328.h @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include + +DEFINE_DEVICETREE(cst328, struct Cst328Config) diff --git a/Drivers/cst328-module/include/cst328_module.h b/Drivers/cst328-module/include/cst328_module.h new file mode 100644 index 000000000..fe1421a22 --- /dev/null +++ b/Drivers/cst328-module/include/cst328_module.h @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +extern struct Module cst328_module; + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/cst328-module/include/drivers/cst328.h b/Drivers/cst328-module/include/drivers/cst328.h new file mode 100644 index 000000000..ec010170b --- /dev/null +++ b/Drivers/cst328-module/include/drivers/cst328.h @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include + +struct Cst328Config { + // Devicetree address hint (0x1A on the LilyGO T-Deck Pro). + uint8_t address; + bool swap_xy; + bool mirror_x; + bool mirror_y; + struct GpioPinSpec pin_reset; + bool reset_active_high; +}; + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/cst328-module/source/cst328.cpp b/Drivers/cst328-module/source/cst328.cpp new file mode 100644 index 000000000..be93eb6c1 --- /dev/null +++ b/Drivers/cst328-module/source/cst328.cpp @@ -0,0 +1,293 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#define TAG "CST328" +#define GET_CONFIG(device) (static_cast((device)->config)) + +static const TickType_t I2C_TIMEOUT = pdMS_TO_TICKS(50); + +// The CST328/CST226SE protocol addresses registers via a 2-byte [class, sub-register] prefix +// (not a plain 1-byte register index), sent as a raw write, optionally followed by a read of the +// response - ported from LilyGO's SensorLib TouchClassCST226.cpp (touch/TouchClassCST226.cpp), +// the vendor driver actually used for the T-Deck Pro's CST328. +static constexpr uint8_t CST328_MAX_POINTS = 5; +static constexpr uint8_t STATUS_BUFFER_SIZE = 28; + +struct Cst328Internal { + int16_t res_x; + int16_t res_y; + bool swap_xy; + bool mirror_x; + bool mirror_y; + uint8_t point_count; + int16_t x[CST328_MAX_POINTS]; + int16_t y[CST328_MAX_POINTS]; + uint8_t strength[CST328_MAX_POINTS]; +}; + +static bool write2(Device* i2c, uint8_t address, uint8_t b0, uint8_t b1) { + uint8_t cmd[2] = { b0, b1 }; + return i2c_controller_write(i2c, address, cmd, sizeof(cmd), I2C_TIMEOUT) == ERROR_NONE; +} + +static bool query(Device* i2c, uint8_t address, uint8_t b0, uint8_t b1, uint8_t* out, size_t out_len) { + uint8_t cmd[2] = { b0, b1 }; + return i2c_controller_write_read(i2c, address, cmd, sizeof(cmd), out, out_len, I2C_TIMEOUT) == ERROR_NONE; +} + +// region Driver lifecycle + +// Some boards wire a dedicated reset GPIO (e.g. the T-Deck Pro's GPIO45); others rely on the +// chip's own software reset command instead. +static void perform_reset(Device* i2c, uint8_t address, const Cst328Config* config) { + if (config->pin_reset.gpio_controller != nullptr) { + auto* rst = gpio_descriptor_acquire(config->pin_reset.gpio_controller, config->pin_reset.pin, GPIO_OWNER_GPIO); + if (rst != nullptr) { + gpio_descriptor_set_flags(rst, GPIO_FLAG_DIRECTION_OUTPUT); + gpio_descriptor_set_level(rst, config->reset_active_high); + delay_millis(100); + gpio_descriptor_set_level(rst, !config->reset_active_high); + gpio_descriptor_release(rst); + delay_millis(100); + return; + } + } + + write2(i2c, address, 0xD1, 0x0E); + delay_millis(20); +} + +// Enters command mode, reads the firmware checkcode and panel resolution, verifies the firmware +// is genuinely present, then exits command mode. Ported from TouchClassCST226::initImpl(). +static bool identify_and_configure(Device* i2c, uint8_t address, Cst328Internal* internal) { + if (!write2(i2c, address, 0xD1, 0x01)) { + return false; + } + delay_millis(10); + + uint8_t buffer[8]; + if (!query(i2c, address, 0xD1, 0xFC, buffer, 4)) { + return false; + } + uint32_t checkcode = (static_cast(buffer[3]) << 24) | (static_cast(buffer[2]) << 16) | + (static_cast(buffer[1]) << 8) | buffer[0]; + + if (!query(i2c, address, 0xD1, 0xF8, buffer, 4)) { + return false; + } + internal->res_x = static_cast((buffer[1] << 8) | buffer[0]); + internal->res_y = static_cast((buffer[3] << 8) | buffer[2]); + + if (!query(i2c, address, 0xD2, 0x08, buffer, 8)) { + return false; + } + uint32_t fw_version = (static_cast(buffer[3]) << 24) | (static_cast(buffer[2]) << 16) | + (static_cast(buffer[1]) << 8) | buffer[0]; + + if (fw_version == 0xA5A5A5A5) { + LOG_E(TAG, "Chip has no firmware"); + return false; + } + if ((checkcode & 0xFFFF0000) != 0xCACA0000) { + LOG_E(TAG, "Firmware info read error"); + return false; + } + + LOG_I(TAG, "CST328 identified: %dx%d, firmware 0x%08lX", internal->res_x, internal->res_y, (unsigned long)fw_version); + return write2(i2c, address, 0xD1, 0x09); // exit command mode +} + +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(calloc(1, sizeof(Cst328Internal))); + if (internal == nullptr) { + return ERROR_OUT_OF_MEMORY; + } + internal->swap_xy = config->swap_xy; + internal->mirror_x = config->mirror_x; + internal->mirror_y = config->mirror_y; + + perform_reset(parent, config->address, config); + + if (!identify_and_configure(parent, config->address, internal)) { + // Non-fatal: matches the tolerant bring-up pattern used by other touch drivers in this + // codebase - a transient bus hiccup on the first transaction after boot shouldn't take + // the whole kernel down. The device just won't report touches until it responds. + LOG_W(TAG, "CST328 identity not confirmed (continuing)"); + } + + device_set_driver_data(device, internal); + 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 + +// region PointerApi + +static error_t cst328_enter_sleep(Device* device) { + auto* parent = device_get_parent(device); + const auto* config = GET_CONFIG(device); + return write2(parent, config->address, 0xD1, 0x05) ? ERROR_NONE : ERROR_RESOURCE; +} + +static error_t cst328_exit_sleep(Device* device) { + auto* parent = device_get_parent(device); + const auto* config = GET_CONFIG(device); + perform_reset(parent, config->address, config); + return ERROR_NONE; +} + +static error_t cst328_read_data(Device* device, TickType_t /*timeout*/) { + auto* internal = static_cast(device_get_driver_data(device)); + const auto* config = GET_CONFIG(device); + auto* parent = device_get_parent(device); + + uint8_t buffer[STATUS_BUFFER_SIZE]; + uint8_t reg = 0x00; + if (i2c_controller_write_read(parent, config->address, ®, 1, buffer, sizeof(buffer), I2C_TIMEOUT) != ERROR_NONE) { + return ERROR_RESOURCE; + } + + // Home-button gesture frame (T-Deck Pro doesn't have one wired, but the vendor protocol + // reports it): no touch point data. + bool is_home_gesture = buffer[0] == 0x83 && buffer[1] == 0x17 && buffer[5] == 0x80; + if (is_home_gesture || buffer[6] != 0xAB || buffer[0] == 0xAB || buffer[5] == 0x80) { + internal->point_count = 0; + return ERROR_NONE; + } + + uint8_t point = buffer[5] & 0x7F; + if (point > CST328_MAX_POINTS || point == 0) { + write2(parent, config->address, 0x00, 0xAB); // acknowledge/clear + internal->point_count = 0; + return ERROR_NONE; + } + + // A lone point whose status nibble isn't 0x06 is a spurious single-touch frame. + uint8_t point1_status = buffer[0] & 0x0F; + if (point == 1 && point1_status != 0x06) { + point = 0; + } + + uint8_t index = 0; + for (uint8_t i = 0; i < point; i++) { + int16_t raw_x = static_cast((buffer[index + 1] << 4) | ((buffer[index + 3] >> 4) & 0x0F)); + int16_t raw_y = static_cast((buffer[index + 2] << 4) | (buffer[index + 3] & 0x0F)); + uint8_t pressure = buffer[index + 4]; + + if (internal->swap_xy) { + std::swap(raw_x, raw_y); + } + if (internal->mirror_x && internal->res_x > 0) { + raw_x = static_cast(internal->res_x - 1 - raw_x); + } + if (internal->mirror_y && internal->res_y > 0) { + raw_y = static_cast(internal->res_y - 1 - raw_y); + } + + internal->x[i] = raw_x; + internal->y[i] = raw_y; + internal->strength[i] = pressure; + + // First point's slot is 7 bytes wide (id/status + x-hi + y-hi + xy-lo + pressure + 2 + // reserved); subsequent points are 5 bytes wide. + index = static_cast((i == 0) ? (index + 7) : (index + 5)); + } + internal->point_count = point; + return ERROR_NONE; +} + +static bool cst328_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)); + uint8_t count = internal->point_count < max_point_count ? internal->point_count : max_point_count; + for (uint8_t i = 0; i < count; i++) { + x[i] = static_cast(internal->x[i]); + y[i] = static_cast(internal->y[i]); + if (strength != nullptr) { + strength[i] = internal->strength[i]; + } + } + *point_count = count; + return count > 0; +} + +static error_t cst328_set_swap_xy(Device* device, bool swap) { + static_cast(device_get_driver_data(device))->swap_xy = swap; + return ERROR_NONE; +} + +static error_t cst328_get_swap_xy(Device* device, bool* swap) { + *swap = static_cast(device_get_driver_data(device))->swap_xy; + return ERROR_NONE; +} + +static error_t cst328_set_mirror_x(Device* device, bool mirror) { + static_cast(device_get_driver_data(device))->mirror_x = mirror; + return ERROR_NONE; +} + +static error_t cst328_get_mirror_x(Device* device, bool* mirror) { + *mirror = static_cast(device_get_driver_data(device))->mirror_x; + return ERROR_NONE; +} + +static error_t cst328_set_mirror_y(Device* device, bool mirror) { + static_cast(device_get_driver_data(device))->mirror_y = mirror; + return ERROR_NONE; +} + +static error_t cst328_get_mirror_y(Device* device, bool* mirror) { + *mirror = static_cast(device_get_driver_data(device))->mirror_y; + return ERROR_NONE; +} + +// endregion + +static const PointerApi cst328_pointer_api = { + .enter_sleep = cst328_enter_sleep, + .exit_sleep = cst328_exit_sleep, + .read_data = cst328_read_data, + .get_touched_points = cst328_get_touched_points, + .set_swap_xy = cst328_set_swap_xy, + .get_swap_xy = cst328_get_swap_xy, + .set_mirror_x = cst328_set_mirror_x, + .get_mirror_x = cst328_get_mirror_x, + .set_mirror_y = cst328_set_mirror_y, + .get_mirror_y = cst328_get_mirror_y, +}; + +Driver cst328_driver = { + .name = "cst328", + .compatible = (const char*[]) { "hynitron,cst328", nullptr }, + .start_device = start, + .stop_device = stop, + .api = &cst328_pointer_api, + .device_type = &POINTER_TYPE, + .owner = &cst328_module, + .internal = nullptr +}; diff --git a/Devices/lilygo-tlora-pager/Source/module.cpp b/Drivers/cst328-module/source/module.cpp similarity index 71% rename from Devices/lilygo-tlora-pager/Source/module.cpp rename to Drivers/cst328-module/source/module.cpp index 5153f1fbf..05b96ac19 100644 --- a/Devices/lilygo-tlora-pager/Source/module.cpp +++ b/Drivers/cst328-module/source/module.cpp @@ -1,31 +1,32 @@ +// SPDX-License-Identifier: Apache-2.0 #include #include #include extern "C" { -extern Driver tlora_pager_driver; +extern Driver cst328_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(&tlora_pager_driver) == ERROR_NONE); + check(driver_construct_add(&cst328_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(&tlora_pager_driver) == ERROR_NONE); + check(driver_remove_destruct(&cst328_driver) == ERROR_NONE); return ERROR_NONE; } -struct Module lilygo_tlora_pager_module = { - .name = "lilygo-tlora-pager", +Module cst328_module = { + .name = "cst328", .start = start, .stop = stop, .symbols = nullptr, .internal = nullptr }; -} +} // extern "C" diff --git a/Drivers/cst66xx-module/CMakeLists.txt b/Drivers/cst66xx-module/CMakeLists.txt new file mode 100644 index 000000000..7469e6726 --- /dev/null +++ b/Drivers/cst66xx-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(cst66xx-module + SRCS ${SOURCE_FILES} + INCLUDE_DIRS include/ + REQUIRES TactilityKernel +) diff --git a/Drivers/cst66xx-module/bindings/hynitron,cst66xx.yaml b/Drivers/cst66xx-module/bindings/hynitron,cst66xx.yaml new file mode 100644 index 000000000..e59632170 --- /dev/null +++ b/Drivers/cst66xx-module/bindings/hynitron,cst66xx.yaml @@ -0,0 +1,41 @@ +description: > + Hynitron CST66xx capacitive touch controller. Vendor docs for the LilyGO T-Deck Max label + this chip "CST328", but the vendor HynTouch driver probes cst66xx first and that is what + answers on that board. This is a minimal, single-touch port of the vendor's + hyn_cst66xx.c protocol. + +include: ["i2c-device.yaml"] + +compatible: "hynitron,cst66xx" + +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 + required: true + description: Reset GPIO pin. The chip only re-initializes into normal reporting mode after a clean reset pulse. + reset-active-high: + type: boolean + default: false + description: Whether the reset pin is active high diff --git a/Drivers/cst66xx-module/devicetree.yaml b/Drivers/cst66xx-module/devicetree.yaml new file mode 100644 index 000000000..a07d6f334 --- /dev/null +++ b/Drivers/cst66xx-module/devicetree.yaml @@ -0,0 +1,3 @@ +dependencies: + - TactilityKernel +bindings: bindings diff --git a/Drivers/cst66xx-module/include/bindings/cst66xx.h b/Drivers/cst66xx-module/include/bindings/cst66xx.h new file mode 100644 index 000000000..a8855aa0d --- /dev/null +++ b/Drivers/cst66xx-module/include/bindings/cst66xx.h @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include + +DEFINE_DEVICETREE(cst66xx, struct Cst66xxConfig) diff --git a/Drivers/cst66xx-module/include/cst66xx_module.h b/Drivers/cst66xx-module/include/cst66xx_module.h new file mode 100644 index 000000000..a30473117 --- /dev/null +++ b/Drivers/cst66xx-module/include/cst66xx_module.h @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +extern struct Module cst66xx_module; + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/cst66xx-module/include/drivers/cst66xx.h b/Drivers/cst66xx-module/include/drivers/cst66xx.h new file mode 100644 index 000000000..2f35b1bc3 --- /dev/null +++ b/Drivers/cst66xx-module/include/drivers/cst66xx.h @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include + +struct Cst66xxConfig { + // Devicetree address hint (0x1A on the LilyGO T-Deck Max). + uint8_t address; + uint16_t x_max; + uint16_t y_max; + bool swap_xy; + bool mirror_x; + bool mirror_y; + struct GpioPinSpec pin_reset; + bool reset_active_high; +}; + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/cst66xx-module/source/cst66xx.cpp b/Drivers/cst66xx-module/source/cst66xx.cpp new file mode 100644 index 000000000..56ae7620d --- /dev/null +++ b/Drivers/cst66xx-module/source/cst66xx.cpp @@ -0,0 +1,266 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#define TAG "CST66xx" +#define GET_CONFIG(device) (static_cast((device)->config)) + +static const TickType_t I2C_TIMEOUT = pdMS_TO_TICKS(20); + +// The protocol uses 4-byte register addresses, ported from the vendor's lib/HynTouch/src/hyn_cst66xx.c +// (see Devices/lilygo-tdeck-max's former Source/devices/Cst66xxTouch.cpp for the pre-migration history). + +// Normal-mode setup sequence (cst66xx_set_workmode, NOMAL_MODE). +static constexpr uint8_t CMD_DISABLE_LP_I2C[4] = { 0xD0, 0x00, 0x04, 0x00 }; +static constexpr uint8_t CMD_NORMAL_A[4] = { 0xD0, 0x00, 0x00, 0x00 }; +static constexpr uint8_t CMD_NORMAL_B[4] = { 0xD0, 0x00, 0x0C, 0x00 }; +static constexpr uint8_t CMD_NORMAL_C[4] = { 0xD0, 0x00, 0x01, 0x00 }; +// Read-point register: write these 4 bytes, then read the touch frame. +static constexpr uint8_t REG_READ_POINT[4] = { 0xD0, 0x07, 0x00, 0x00 }; +// Acknowledge/clear after each read. +static constexpr uint8_t CMD_ACK[4] = { 0xD0, 0x00, 0x02, 0xAB }; +// Identity/config register (cst66xx_updata_tpinfo): read 50 bytes; buf[2]==0xCA && buf[3]==0xCA confirms a CST66xx. +static constexpr uint8_t REG_INFO[4] = { 0xD0, 0x03, 0x00, 0x00 }; + +struct Cst66xxInternal { + int16_t last_x; + int16_t last_y; + bool has_point; + bool swap_xy; + bool mirror_x; + bool mirror_y; +}; + +// region Driver lifecycle + +static void set_normal_mode(Device* i2c, uint8_t address) { + i2c_controller_write(i2c, address, CMD_DISABLE_LP_I2C, sizeof(CMD_DISABLE_LP_I2C), I2C_TIMEOUT); + delay_millis(1); + i2c_controller_write(i2c, address, CMD_DISABLE_LP_I2C, sizeof(CMD_DISABLE_LP_I2C), I2C_TIMEOUT); + i2c_controller_write(i2c, address, CMD_NORMAL_A, sizeof(CMD_NORMAL_A), I2C_TIMEOUT); + i2c_controller_write(i2c, address, CMD_NORMAL_B, sizeof(CMD_NORMAL_B), I2C_TIMEOUT); + i2c_controller_write(i2c, address, CMD_NORMAL_C, sizeof(CMD_NORMAL_C), I2C_TIMEOUT); +} + +static void perform_hardware_reset(const Cst66xxConfig* config) { + if (config->pin_reset.gpio_controller == nullptr) { + return; + } + + auto* rst = gpio_descriptor_acquire(config->pin_reset.gpio_controller, config->pin_reset.pin, GPIO_OWNER_GPIO); + if (rst == nullptr) { + return; + } + + gpio_descriptor_set_flags(rst, GPIO_FLAG_DIRECTION_OUTPUT); + gpio_descriptor_set_level(rst, config->reset_active_high); + delay_millis(10); + gpio_descriptor_set_level(rst, !config->reset_active_high); + gpio_descriptor_release(rst); + // The reset pulse exits boot mode; give the controller time to re-init. + delay_millis(80); +} + +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(Cst66xxInternal))); + if (internal == nullptr) { + return ERROR_OUT_OF_MEMORY; + } + internal->last_x = 0; + internal->last_y = 0; + internal->has_point = false; + internal->swap_xy = config->swap_xy; + internal->mirror_x = config->mirror_x; + internal->mirror_y = config->mirror_y; + + perform_hardware_reset(config); + + // Put the controller into normal reporting mode and verify the identity. The first read + // after reset can miss, so retry like the vendor's cst66xx_updata_tpinfo (read 0xD0030000, + // expect buf[2]==buf[3]==0xCA). + bool confirmed = false; + for (int attempt = 0; attempt < 5 && !confirmed; ++attempt) { + set_normal_mode(parent, config->address); + uint8_t info[50] = {}; + if (i2c_controller_write(parent, config->address, REG_INFO, sizeof(REG_INFO), I2C_TIMEOUT) == ERROR_NONE && + i2c_controller_read(parent, config->address, info, sizeof(info), I2C_TIMEOUT) == ERROR_NONE && + info[2] == 0xCA && info[3] == 0xCA) { + confirmed = true; + } else { + delay_millis(10); + } + } + if (confirmed) { + LOG_I(TAG, "CST66xx initialised"); + } else { + LOG_W(TAG, "CST66xx identity not confirmed (continuing)"); + } + + device_set_driver_data(device, internal); + 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 + +// region PointerApi + +static error_t cst66xx_read_data(Device* device, TickType_t /*timeout*/) { + auto* internal = static_cast(device_get_driver_data(device)); + const auto* config = GET_CONFIG(device); + auto* parent = device_get_parent(device); + + // CST66xx frame (cst66xx_report): write the read-point register, then read 9 bytes. + // buf[2]=report type (0xFF=position/key), buf[3] low nibble = finger count, high nibble = + // key count. The first 5-byte slot (buf[4..8]) is a key slot when key count > 0, otherwise + // the first finger; further fingers, if any, follow at buf[index+...]. + uint8_t buf[9] = {}; + error_t err = i2c_controller_write(parent, config->address, REG_READ_POINT, sizeof(REG_READ_POINT), I2C_TIMEOUT); + if (err == ERROR_NONE) { + err = i2c_controller_read(parent, config->address, buf, sizeof(buf), I2C_TIMEOUT); + } + if (err != ERROR_NONE) { + return ERROR_RESOURCE; + } + + // Acknowledge/clear the frame after every read. + i2c_controller_write(parent, config->address, CMD_ACK, sizeof(CMD_ACK), I2C_TIMEOUT); + + const uint8_t reportType = buf[2]; + const uint8_t fingerCount = buf[3] & 0x0F; + const uint8_t keyCount = (buf[3] & 0xF0) >> 4; + + // The controller also reports the three bezel touch-keys (heart/speech-bubble/paper-airplane) + // mutually exclusively with finger coordinates in this same frame shape. Kernel drivers don't + // have a home for UI-navigation policy (see the CST66xx driver's module README), so key frames + // are just treated as "no touch" here. + if ((reportType == 0xFF && keyCount > 0) || reportType != 0xFF || fingerCount < 1) { + internal->has_point = false; + return ERROR_NONE; + } + + // First finger's slot follows any key slots. + const uint8_t index = keyCount * 5; + const uint8_t event = buf[index + 8] >> 4; // 0 = up + if (event == 0) { + internal->has_point = false; + return ERROR_NONE; + } + + int16_t rawX = static_cast(buf[index + 4] | (static_cast(buf[index + 7] & 0x0F) << 8)); + int16_t rawY = static_cast(buf[index + 5] | (static_cast(buf[index + 7] & 0xF0) << 4)); + + if (internal->swap_xy) { + std::swap(rawX, rawY); + } + if (internal->mirror_x) { + rawX = static_cast(config->x_max - 1 - rawX); + } + if (internal->mirror_y) { + rawY = static_cast(config->y_max - 1 - rawY); + } + + internal->last_x = rawX; + internal->last_y = rawY; + internal->has_point = true; + return ERROR_NONE; +} + +static bool cst66xx_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)); + if (!internal->has_point || max_point_count < 1) { + *point_count = 0; + return false; + } + + x[0] = static_cast(internal->last_x); + y[0] = static_cast(internal->last_y); + if (strength != nullptr) { + strength[0] = 0; + } + *point_count = 1; + return true; +} + +static error_t cst66xx_set_swap_xy(Device* device, bool swap) { + static_cast(device_get_driver_data(device))->swap_xy = swap; + return ERROR_NONE; +} + +static error_t cst66xx_get_swap_xy(Device* device, bool* swap) { + *swap = static_cast(device_get_driver_data(device))->swap_xy; + return ERROR_NONE; +} + +static error_t cst66xx_set_mirror_x(Device* device, bool mirror) { + static_cast(device_get_driver_data(device))->mirror_x = mirror; + return ERROR_NONE; +} + +static error_t cst66xx_get_mirror_x(Device* device, bool* mirror) { + *mirror = static_cast(device_get_driver_data(device))->mirror_x; + return ERROR_NONE; +} + +static error_t cst66xx_set_mirror_y(Device* device, bool mirror) { + static_cast(device_get_driver_data(device))->mirror_y = mirror; + return ERROR_NONE; +} + +static error_t cst66xx_get_mirror_y(Device* device, bool* mirror) { + *mirror = static_cast(device_get_driver_data(device))->mirror_y; + return ERROR_NONE; +} + +// enter_sleep/exit_sleep are not exposed: the ported protocol has no equivalent commands. + +// endregion + +static const PointerApi cst66xx_pointer_api = { + .enter_sleep = nullptr, + .exit_sleep = nullptr, + .read_data = cst66xx_read_data, + .get_touched_points = cst66xx_get_touched_points, + .set_swap_xy = cst66xx_set_swap_xy, + .get_swap_xy = cst66xx_get_swap_xy, + .set_mirror_x = cst66xx_set_mirror_x, + .get_mirror_x = cst66xx_get_mirror_x, + .set_mirror_y = cst66xx_set_mirror_y, + .get_mirror_y = cst66xx_get_mirror_y, +}; + +Driver cst66xx_driver = { + .name = "cst66xx", + .compatible = (const char*[]) { "hynitron,cst66xx", nullptr }, + .start_device = start, + .stop_device = stop, + .api = &cst66xx_pointer_api, + .device_type = &POINTER_TYPE, + .owner = &cst66xx_module, + .internal = nullptr +}; diff --git a/Drivers/cst66xx-module/source/module.cpp b/Drivers/cst66xx-module/source/module.cpp new file mode 100644 index 000000000..a5c77be56 --- /dev/null +++ b/Drivers/cst66xx-module/source/module.cpp @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include + +extern "C" { + +extern Driver cst66xx_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(&cst66xx_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(&cst66xx_driver) == ERROR_NONE); + return ERROR_NONE; +} + +Module cst66xx_module = { + .name = "cst66xx", + .start = start, + .stop = stop, + .symbols = nullptr, + .internal = nullptr +}; + +} // extern "C" diff --git a/Drivers/drv2605-module/CMakeLists.txt b/Drivers/drv2605-module/CMakeLists.txt new file mode 100644 index 000000000..fdccad819 --- /dev/null +++ b/Drivers/drv2605-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(drv2605-module + SRCS ${SOURCE_FILES} + INCLUDE_DIRS include/ + REQUIRES TactilityKernel +) diff --git a/Drivers/drv2605-module/bindings/ti,drv2605.yaml b/Drivers/drv2605-module/bindings/ti,drv2605.yaml new file mode 100644 index 000000000..f60a5de85 --- /dev/null +++ b/Drivers/drv2605-module/bindings/ti,drv2605.yaml @@ -0,0 +1,23 @@ +description: > + Texas Instruments DRV2605/DRV2604 (and low-voltage L-variant) haptic motor driver for ERM/LRA + actuators, with an onboard waveform effect library. Brought up in open-loop ERM mode (matches + the deprecated HAL's Drv2605 - closed-loop/LRA configuration isn't ported). Exposes a generic + "haptic" device (see tactility/drivers/haptic.h) for playing further effects at runtime. + +include: ["i2c-device.yaml"] + +compatible: "ti,drv2605" + +bus: i2c + +properties: + autoplay: + type: boolean + default: true + description: Play startup-waveform once at start-up (e.g. a startup "buzz" to confirm the motor works) + startup-waveform: + type: array + element-type: uint8_t + description: > + Effect sequence (waveform library indices, 0 ends the sequence) played once at start-up + when autoplay is set. Up to 8 bytes. Omit for the default 3x strong-click "buzz" ({1,1,1}). diff --git a/Drivers/drv2605-module/devicetree.yaml b/Drivers/drv2605-module/devicetree.yaml new file mode 100644 index 000000000..a07d6f334 --- /dev/null +++ b/Drivers/drv2605-module/devicetree.yaml @@ -0,0 +1,3 @@ +dependencies: + - TactilityKernel +bindings: bindings diff --git a/Drivers/drv2605-module/include/bindings/drv2605.h b/Drivers/drv2605-module/include/bindings/drv2605.h new file mode 100644 index 000000000..b2d9db74f --- /dev/null +++ b/Drivers/drv2605-module/include/bindings/drv2605.h @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include + +DEFINE_DEVICETREE(drv2605, struct Drv2605Config) diff --git a/Drivers/drv2605-module/include/drivers/drv2605.h b/Drivers/drv2605-module/include/drivers/drv2605.h new file mode 100644 index 000000000..d7c9eaea5 --- /dev/null +++ b/Drivers/drv2605-module/include/drivers/drv2605.h @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +struct Drv2605Config { + // Devicetree address hint (0x5A). + uint8_t address; + bool autoplay; + // WaveSequence1.. effect slots to play once at start-up when autoplay is set. NULL/0-length + // falls back to a 3x strong-click "buzz" ({1,1,1}). + const uint8_t* startup_waveform; + uint32_t startup_waveform_length; +}; + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/drv2605-module/include/drv2605_module.h b/Drivers/drv2605-module/include/drv2605_module.h new file mode 100644 index 000000000..2cf7942bf --- /dev/null +++ b/Drivers/drv2605-module/include/drv2605_module.h @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +extern struct Module drv2605_module; + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/drv2605-module/source/drv2605.cpp b/Drivers/drv2605-module/source/drv2605.cpp new file mode 100644 index 000000000..5b3c4f7b5 --- /dev/null +++ b/Drivers/drv2605-module/source/drv2605.cpp @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include + +#include +#include +#include +#include +#include +#include + +#define TAG "DRV2605" +#define GET_CONFIG(device) (static_cast((device)->config)) + +static const TickType_t I2C_TIMEOUT = pdMS_TO_TICKS(50); + +// DRV260x register map (ported from the deprecated HAL's Drv2605.h/.cpp). +static constexpr uint8_t REG_STATUS = 0x00; +static constexpr uint8_t REG_MODE = 0x01; +static constexpr uint8_t REG_REALTIME_PLAYBACK_INPUT = 0x02; +static constexpr uint8_t REG_WAVE_LIBRARY_SELECT = 0x03; +static constexpr uint8_t REG_WAVE_SEQUENCE_1 = 0x04; // WaveSequence1..8 = 0x04..0x0B +static constexpr uint8_t REG_GO = 0x0C; +static constexpr uint8_t REG_OVERDRIVE_TIME_OFFSET = 0x0D; +static constexpr uint8_t REG_SUSTAIN_TIME_OFFSET_POSITIVE = 0x0E; +static constexpr uint8_t REG_SUSTAIN_TIME_OFFSET_NEGATIVE = 0x0F; +static constexpr uint8_t REG_BRAKE_TIME_OFFSET = 0x10; +static constexpr uint8_t REG_AUDIO_INPUT_LEVEL_MAX = 0x13; +static constexpr uint8_t REG_FEEDBACK = 0x1A; +static constexpr uint8_t REG_CONTROL3 = 0x1D; + +static constexpr uint8_t FEEDBACK_N_ERM_LRA = 1u << 7; +static constexpr uint8_t CONTROL3_ERM_OPEN_LOOP = 1u << 5; + +// 3x strong-click "buzz", matching the deprecated HAL's Drv2605::setWaveFormForBuzz(). +static constexpr uint8_t DEFAULT_STARTUP_WAVEFORM[] = { 1, 1, 1, 0 }; + +extern "C" { + +// region HapticApi + +static error_t set_waveform(Device* device, uint8_t slot, uint8_t waveform) { + const auto* config = GET_CONFIG(device); + auto* i2c = device_get_parent(device); + return i2c_controller_register8_set(i2c, config->address, static_cast(REG_WAVE_SEQUENCE_1 + slot), waveform, I2C_TIMEOUT); +} + +static error_t select_library(Device* device, uint8_t library) { + const auto* config = GET_CONFIG(device); + auto* i2c = device_get_parent(device); + return i2c_controller_register8_set(i2c, config->address, REG_WAVE_LIBRARY_SELECT, library, I2C_TIMEOUT); +} + +static error_t start_playback(Device* device) { + const auto* config = GET_CONFIG(device); + auto* i2c = device_get_parent(device); + return i2c_controller_register8_set(i2c, config->address, REG_GO, 0x01, I2C_TIMEOUT); +} + +static error_t stop_playback(Device* device) { + const auto* config = GET_CONFIG(device); + auto* i2c = device_get_parent(device); + return i2c_controller_register8_set(i2c, config->address, REG_GO, 0x00, I2C_TIMEOUT); +} + +static constexpr HapticApi DRV2605_HAPTIC_API = { + .set_waveform = set_waveform, + .select_library = select_library, + .start_playback = start_playback, + .stop_playback = stop_playback, +}; + +// endregion + +// region Driver lifecycle + +static error_t write_waveform_sequence(Device* i2c, uint8_t address, const uint8_t* waveform, uint32_t length) { + for (uint32_t i = 0; i < length && i < 8; i++) { + error_t error = i2c_controller_register8_set(i2c, address, static_cast(REG_WAVE_SEQUENCE_1 + i), waveform[i], I2C_TIMEOUT); + if (error != ERROR_NONE) { + return error; + } + } + return ERROR_NONE; +} + +static error_t start(Device* device) { + auto* i2c = device_get_parent(device); + check(device_get_type(i2c) == &I2C_CONTROLLER_TYPE); + const auto* config = GET_CONFIG(device); + + uint8_t status = 0; + if (i2c_controller_register8_get(i2c, config->address, REG_STATUS, &status, I2C_TIMEOUT) != ERROR_NONE) { + LOG_E(TAG, "Failed to read status"); + return ERROR_RESOURCE; + } + + // Bits[7:5]: 3=DRV2605, 4=DRV2604, 6=DRV2604L, 7=DRV2605L. + uint8_t chip_id = status >> 5; + if (chip_id != 3 && chip_id != 4 && chip_id != 6 && chip_id != 7) { + LOG_E(TAG, "Unknown chip id %02X", chip_id); + return ERROR_RESOURCE; + } + + i2c_controller_register8_set(i2c, config->address, REG_MODE, 0x00, I2C_TIMEOUT); // Exit standby + i2c_controller_register8_set(i2c, config->address, REG_REALTIME_PLAYBACK_INPUT, 0x00, I2C_TIMEOUT); // Disable + + // Baseline single strong-click sequence (matches Drv2605::setWaveFormForClick()). + write_waveform_sequence(i2c, config->address, (const uint8_t[]) { 1, 0 }, 2); + i2c_controller_register8_set(i2c, config->address, REG_OVERDRIVE_TIME_OFFSET, 0x00, I2C_TIMEOUT); + i2c_controller_register8_set(i2c, config->address, REG_SUSTAIN_TIME_OFFSET_POSITIVE, 0x00, I2C_TIMEOUT); + i2c_controller_register8_set(i2c, config->address, REG_SUSTAIN_TIME_OFFSET_NEGATIVE, 0x00, I2C_TIMEOUT); + i2c_controller_register8_set(i2c, config->address, REG_BRAKE_TIME_OFFSET, 0x00, I2C_TIMEOUT); + i2c_controller_register8_set(i2c, config->address, REG_AUDIO_INPUT_LEVEL_MAX, 0x64, I2C_TIMEOUT); + + // ERM open loop. + i2c_controller_register8_reset_bits(i2c, config->address, REG_FEEDBACK, FEEDBACK_N_ERM_LRA, I2C_TIMEOUT); + i2c_controller_register8_set_bits(i2c, config->address, REG_CONTROL3, CONTROL3_ERM_OPEN_LOOP, I2C_TIMEOUT); + + if (config->autoplay) { + const uint8_t* waveform = config->startup_waveform; + uint32_t length = config->startup_waveform_length; + if (waveform == nullptr || length == 0) { + waveform = DEFAULT_STARTUP_WAVEFORM; + length = sizeof(DEFAULT_STARTUP_WAVEFORM); + } + write_waveform_sequence(i2c, config->address, waveform, length); + i2c_controller_register8_set(i2c, config->address, REG_GO, 0x01, I2C_TIMEOUT); + } + + return ERROR_NONE; +} + +static error_t stop(Device*) { + return ERROR_NONE; +} + +// endregion + +Driver drv2605_driver = { + .name = "drv2605", + .compatible = (const char*[]) { "ti,drv2605", nullptr }, + .start_device = start, + .stop_device = stop, + .api = &DRV2605_HAPTIC_API, + .device_type = &HAPTIC_TYPE, + .owner = &drv2605_module, + .internal = nullptr +}; + +} diff --git a/Drivers/drv2605-module/source/module.cpp b/Drivers/drv2605-module/source/module.cpp new file mode 100644 index 000000000..f4ecbfa49 --- /dev/null +++ b/Drivers/drv2605-module/source/module.cpp @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include + +extern "C" { + +extern Driver drv2605_driver; + +static error_t start() { + check(driver_construct_add(&drv2605_driver) == ERROR_NONE); + return ERROR_NONE; +} + +static error_t stop() { + check(driver_remove_destruct(&drv2605_driver) == ERROR_NONE); + return ERROR_NONE; +} + +Module drv2605_module = { + .name = "drv2605", + .start = start, + .stop = stop, + .symbols = nullptr, + .internal = nullptr +}; + +} // extern "C" diff --git a/Drivers/jd9165-module/CMakeLists.txt b/Drivers/jd9165-module/CMakeLists.txt new file mode 100644 index 000000000..614267528 --- /dev/null +++ b/Drivers/jd9165-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(jd9165-module + SRCS ${SOURCE_FILES} + INCLUDE_DIRS include/ + REQUIRES TactilityKernel platform-esp32 esp_lcd_jd9165 esp_lcd driver esp_hw_support +) diff --git a/Drivers/jd9165-module/LICENSE-Apache-2.0.md b/Drivers/jd9165-module/LICENSE-Apache-2.0.md new file mode 100644 index 000000000..f5f4b8b5e --- /dev/null +++ b/Drivers/jd9165-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/jd9165-module/bindings/jdi,jd9165.yaml b/Drivers/jd9165-module/bindings/jdi,jd9165.yaml new file mode 100644 index 000000000..4ed0900be --- /dev/null +++ b/Drivers/jd9165-module/bindings/jdi,jd9165.yaml @@ -0,0 +1,126 @@ +description: > + JDI JD9165 MIPI-DSI display panel, driven over ESP-IDF's esp_lcd_jd9165 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: "jdi,jd9165" + +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 JD9165 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/jd9165-module/devicetree.yaml b/Drivers/jd9165-module/devicetree.yaml new file mode 100644 index 000000000..a07d6f334 --- /dev/null +++ b/Drivers/jd9165-module/devicetree.yaml @@ -0,0 +1,3 @@ +dependencies: + - TactilityKernel +bindings: bindings diff --git a/Drivers/jd9165-module/include/bindings/jd9165.h b/Drivers/jd9165-module/include/bindings/jd9165.h new file mode 100644 index 000000000..88ee2cf10 --- /dev/null +++ b/Drivers/jd9165-module/include/bindings/jd9165.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. "jdi,jd9165" -> jd9165_config_dt), not from the node name or driver +// name, so the tag here must match that exactly. +DEFINE_DEVICETREE(jd9165, struct Jd9165Config) diff --git a/Drivers/jd9165-module/include/drivers/jd9165.h b/Drivers/jd9165-module/include/drivers/jd9165.h new file mode 100644 index 000000000..beb975eb1 --- /dev/null +++ b/Drivers/jd9165-module/include/drivers/jd9165.h @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include +#include + +struct Jd9165Config { + 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_jd9165's panel_jd9165_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 + // JD9165 component's own built-in default sequence (see vendor_specific_init_default in + // esp_lcd_jd9165.c) - just a sleep-out + display-on, 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/jd9165-module/include/jd9165_module.h b/Drivers/jd9165-module/include/jd9165_module.h new file mode 100644 index 000000000..045a4912b --- /dev/null +++ b/Drivers/jd9165-module/include/jd9165_module.h @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +extern struct Module jd9165_module; + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/jd9165-module/source/jd9165.cpp b/Drivers/jd9165-module/source/jd9165.cpp new file mode 100644 index 000000000..1352a74ed --- /dev/null +++ b/Drivers/jd9165-module/source/jd9165.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 + +#define TAG "Jd9165" +#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 Jd9165Internal { + 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(). + jd9165_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/jdi,jd9165.yaml) into a heap-allocated jd9165_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, jd9165_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(jd9165_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(Jd9165Internal))); + if (internal == nullptr) { + return ERROR_OUT_OF_MEMORY; + } + internal->parsed_init_cmds = nullptr; + + const jd9165_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, + }, + }; + + jd9165_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_jd9165(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 JD9165 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 jd9165_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 jd9165_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 jd9165_color_data_is_frame_buffer(const Jd9165Internal* 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 jd9165_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 && jd9165_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_jd9165 - see +// panel_jd9165_mirror() in esp_lcd_jd9165.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 jd9165_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 jd9165_get_mirror_x(Device* device) { + return GET_CONFIG(device)->mirror_x; +} + +static bool jd9165_get_mirror_y(Device* device) { + return GET_CONFIG(device)->mirror_y; +} + +// swap_xy/set_gap are not exposed: esp_lcd_jd9165 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 jd9165_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 jd9165_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_jd9165 doesn't override it either (only +// del/init/reset/mirror/invert_color/disp_on_off - see esp_lcd_jd9165.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 jd9165_get_color_format(Device* device) { + return GET_CONFIG(device)->bits_per_pixel == 24 ? DISPLAY_COLOR_FORMAT_RGB888 : DISPLAY_COLOR_FORMAT_RGB565; +} + +static uint16_t jd9165_get_resolution_x(Device* device) { + return GET_CONFIG(device)->horizontal_resolution; +} + +static uint16_t jd9165_get_resolution_y(Device* device) { + return GET_CONFIG(device)->vertical_resolution; +} + +static void jd9165_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 jd9165_get_frame_buffer_count(Device* device) { + auto* internal = static_cast(device_get_driver_data(device)); + return internal->frame_buffer_count; +} + +static error_t jd9165_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 jd9165_display_api = { + .capabilities = DISPLAY_CAPABILITY_CAP_MIRROR | DISPLAY_CAPABILITY_INVERT_COLOR | + DISPLAY_CAPABILITY_ON_OFF | DISPLAY_CAPABILITY_BACKLIGHT, + .reset = jd9165_reset, + .init = jd9165_init, + .draw_bitmap = jd9165_draw_bitmap, + .mirror = jd9165_mirror, + .swap_xy = nullptr, + .get_swap_xy = nullptr, + .get_mirror_x = jd9165_get_mirror_x, + .get_mirror_y = jd9165_get_mirror_y, + .set_gap = nullptr, + .invert_color = jd9165_invert_color, + .disp_on_off = jd9165_disp_on_off, + .disp_sleep = nullptr, + .get_color_format = jd9165_get_color_format, + .get_resolution_x = jd9165_get_resolution_x, + .get_resolution_y = jd9165_get_resolution_y, + .get_frame_buffer = jd9165_get_frame_buffer, + .get_frame_buffer_count = jd9165_get_frame_buffer_count, + .get_backlight = jd9165_get_backlight, + .has_capability = nullptr, +}; + +Driver jd9165_driver = { + .name = "jd9165", + .compatible = (const char*[]) { "jdi,jd9165", nullptr }, + .start_device = start, + .stop_device = stop, + .api = &jd9165_display_api, + .device_type = &DISPLAY_TYPE, + .owner = &jd9165_module, + .internal = nullptr +}; + +#endif // SOC_MIPI_DSI_SUPPORTED diff --git a/Drivers/jd9165-module/source/module.cpp b/Drivers/jd9165-module/source/module.cpp new file mode 100644 index 000000000..1171609f0 --- /dev/null +++ b/Drivers/jd9165-module/source/module.cpp @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include + +extern "C" { + +extern Driver jd9165_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(&jd9165_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(&jd9165_driver) == ERROR_NONE); + return ERROR_NONE; +} + +Module jd9165_module = { + .name = "jd9165", + .start = start, + .stop = stop, + .symbols = nullptr, + .internal = nullptr +}; + +} // extern "C" diff --git a/Drivers/lilygo-module/CMakeLists.txt b/Drivers/lilygo-module/CMakeLists.txt index 0b2db8912..f6afb3331 100644 --- a/Drivers/lilygo-module/CMakeLists.txt +++ b/Drivers/lilygo-module/CMakeLists.txt @@ -7,5 +7,5 @@ file(GLOB_RECURSE SOURCE_FILES "source/*.c*") tactility_add_module(lilygo-module SRCS ${SOURCE_FILES} INCLUDE_DIRS include/ - REQUIRES TactilityKernel Tactility lvgl + REQUIRES TactilityKernel Tactility lvgl driver ) diff --git a/Drivers/lilygo-module/bindings/lilygo,tpager-encoder.yaml b/Drivers/lilygo-module/bindings/lilygo,tpager-encoder.yaml new file mode 100644 index 000000000..48db9625f --- /dev/null +++ b/Drivers/lilygo-module/bindings/lilygo,tpager-encoder.yaml @@ -0,0 +1,22 @@ +description: > + LilyGO T-Lora Pager encoder wheel next to the display - a 2-phase quadrature encoder + (decoded via the ESP32 hardware PCNT peripheral) plus a separate click/enter button. + Reports raw, unscaled pulses and button level: the pulses-per-detent scaling and enter-press + debounce are UI concerns layered on top by the consumer (see tpager_encoder_input.h), not + something this driver knows about. + +compatible: "lilygo,tpager-encoder" + +properties: + pin-a: + type: phandles + required: true + description: Quadrature phase A GPIO pin + pin-b: + type: phandles + required: true + description: Quadrature phase B GPIO pin + pin-enter: + type: phandles + required: true + description: Click/enter button GPIO pin (active low) diff --git a/Drivers/lilygo-module/include/lilygo/bindings/tpager_encoder.h b/Drivers/lilygo-module/include/lilygo/bindings/tpager_encoder.h new file mode 100644 index 000000000..1d4f5e08a --- /dev/null +++ b/Drivers/lilygo-module/include/lilygo/bindings/tpager_encoder.h @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include + +DEFINE_DEVICETREE(tpager_encoder, struct TpagerEncoderConfig) diff --git a/Drivers/lilygo-module/include/lilygo/drivers/tpager_encoder.h b/Drivers/lilygo-module/include/lilygo/drivers/tpager_encoder.h new file mode 100644 index 000000000..982af26b8 --- /dev/null +++ b/Drivers/lilygo-module/include/lilygo/drivers/tpager_encoder.h @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include +#include + +struct Device; +struct DeviceType; + +struct TpagerEncoderConfig { + struct GpioPinSpec pin_a; + struct GpioPinSpec pin_b; + struct GpioPinSpec pin_enter; +}; + +/** + * @brief API for the T-Lora Pager encoder wheel driver. + * Reports raw, unscaled pulses: pulses-per-detent scaling and enter-press debounce are UI + * concerns layered on top by the consumer, not something this driver knows about. + */ +struct TpagerEncoderApi { + /** + * @brief Reads the accumulated pulse count since the last read, then resets it to zero. + * @param[in] device the encoder device + * @param[out] out_pulses accumulated quadrature pulses (positive/negative by direction) + * @retval ERROR_NONE when the operation was successful + */ + error_t (*read_delta)(struct Device* device, int32_t* out_pulses); + + /** + * @brief Gets whether the enter button is currently pressed. + * @param[in] device the encoder device + * @param[out] out_pressed true when pressed + * @retval ERROR_NONE when the operation was successful + */ + error_t (*get_button_pressed)(struct Device* device, bool* out_pressed); +}; + +/** + * @brief Reads the accumulated pulse count using the specified encoder device. + */ +error_t tpager_encoder_read_delta(struct Device* device, int32_t* out_pulses); + +/** + * @brief Gets whether the enter button is currently pressed on the specified encoder device. + */ +error_t tpager_encoder_get_button_pressed(struct Device* device, bool* out_pressed); + +extern const struct DeviceType TPAGER_ENCODER_TYPE; + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/lilygo-module/include/lilygo/drivers/tpager_encoder_input.h b/Drivers/lilygo-module/include/lilygo/drivers/tpager_encoder_input.h new file mode 100644 index 000000000..f26dbb5af --- /dev/null +++ b/Drivers/lilygo-module/include/lilygo/drivers/tpager_encoder_input.h @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +namespace tpager_encoder { + +/** + * @brief Initialize the encoder wheel as an LVGL input device, backed by the kernel + * tpager_encoder driver. + * @return LVGL input device pointer, or nullptr if the kernel device isn't found/started + */ +lv_indev_t* init(); + +/** + * @brief Deinitialize the encoder wheel's LVGL input device. + */ +void deinit(); + +} diff --git a/Drivers/lilygo-module/source/module.cpp b/Drivers/lilygo-module/source/module.cpp index a166f3fa7..aae34d690 100644 --- a/Drivers/lilygo-module/source/module.cpp +++ b/Drivers/lilygo-module/source/module.cpp @@ -8,6 +8,7 @@ extern "C" { extern Driver tdeck_keyboard_driver; extern Driver tdeck_keyboard_backlight_driver; extern Driver tdeck_trackball_driver; +extern Driver tpager_encoder_driver; static error_t start() { /* We crash when construct fails, because if a single driver fails to construct, @@ -15,6 +16,7 @@ static error_t start() { check(driver_construct_add(&tdeck_keyboard_driver) == ERROR_NONE); check(driver_construct_add(&tdeck_keyboard_backlight_driver) == ERROR_NONE); check(driver_construct_add(&tdeck_trackball_driver) == ERROR_NONE); + check(driver_construct_add(&tpager_encoder_driver) == ERROR_NONE); return ERROR_NONE; } @@ -24,6 +26,7 @@ static error_t stop() { check(driver_remove_destruct(&tdeck_keyboard_driver) == ERROR_NONE); check(driver_remove_destruct(&tdeck_keyboard_backlight_driver) == ERROR_NONE); check(driver_remove_destruct(&tdeck_trackball_driver) == ERROR_NONE); + check(driver_remove_destruct(&tpager_encoder_driver) == ERROR_NONE); return ERROR_NONE; } diff --git a/Drivers/lilygo-module/source/tpager_encoder.cpp b/Drivers/lilygo-module/source/tpager_encoder.cpp new file mode 100644 index 000000000..099427f2a --- /dev/null +++ b/Drivers/lilygo-module/source/tpager_encoder.cpp @@ -0,0 +1,213 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +#include +#include +#include +#include +#include + +#include + +#include + +#define TAG "tpager_encoder" +#define GET_CONFIG(device) (static_cast((device)->config)) +#define GET_INTERNAL(device) (static_cast(device_get_driver_data(device))) + +struct TpagerEncoderInternal { + pcnt_unit_handle_t pcnt_unit = nullptr; + GpioDescriptor* pin_enter = nullptr; +}; + +extern "C" { + +static error_t read_delta(Device* device, int32_t* out_pulses) { + auto* internal = GET_INTERNAL(device); + int pulses = 0; + pcnt_unit_get_count(internal->pcnt_unit, &pulses); + pcnt_unit_clear_count(internal->pcnt_unit); + *out_pulses = pulses; + return ERROR_NONE; +} + +static error_t get_button_pressed(Device* device, bool* out_pressed) { + auto* internal = GET_INTERNAL(device); + bool high = true; + error_t error = gpio_descriptor_get_level(internal->pin_enter, &high); + if (error != ERROR_NONE) { + return error; + } + // Active low: pressed when level is low. + *out_pressed = !high; + return ERROR_NONE; +} + +error_t tpager_encoder_read_delta(Device* device, int32_t* out_pulses) { + return read_delta(device, out_pulses); +} + +error_t tpager_encoder_get_button_pressed(Device* device, bool* out_pressed) { + return get_button_pressed(device, out_pressed); +} + +// region Driver lifecycle + +// Accumulating count makes over-/underflow automatically compensated; requires watch points at +// the low and high limits (see pcnt_unit_add_watch_point() below). Ported from the deprecated +// HAL's TpagerEncoder::initEncoder(). +static constexpr int PCNT_LOW_LIMIT = -127; +static constexpr int PCNT_HIGH_LIMIT = 126; + +static error_t init_pcnt_unit(const TpagerEncoderConfig* config, pcnt_unit_handle_t* out_unit) { + pcnt_unit_config_t unit_config = { + .low_limit = PCNT_LOW_LIMIT, + .high_limit = PCNT_HIGH_LIMIT, + .intr_priority = 0, + .flags = { .accum_count = 1 }, + }; + + pcnt_unit_handle_t unit = nullptr; + if (pcnt_new_unit(&unit_config, &unit) != ESP_OK) { + LOG_E(TAG, "Pulse counter initialization failed"); + return ERROR_RESOURCE; + } + + pcnt_glitch_filter_config_t filter_config = { .max_glitch_ns = 1000 }; + if (pcnt_unit_set_glitch_filter(unit, &filter_config) != ESP_OK) { + LOG_E(TAG, "Pulse counter glitch filter config failed"); + pcnt_del_unit(unit); + return ERROR_RESOURCE; + } + + pcnt_chan_config_t chan_a_config = { + .edge_gpio_num = static_cast(config->pin_b.pin), + .level_gpio_num = static_cast(config->pin_a.pin), + .flags = {}, + }; + pcnt_chan_config_t chan_b_config = { + .edge_gpio_num = static_cast(config->pin_a.pin), + .level_gpio_num = static_cast(config->pin_b.pin), + .flags = {}, + }; + + pcnt_channel_handle_t chan_a = nullptr; + pcnt_channel_handle_t chan_b = nullptr; + if (pcnt_new_channel(unit, &chan_a_config, &chan_a) != ESP_OK || + pcnt_new_channel(unit, &chan_b_config, &chan_b) != ESP_OK) { + LOG_E(TAG, "Pulse counter channel config failed"); + pcnt_del_unit(unit); + return ERROR_RESOURCE; + } + + // Standard quadrature decode: each channel counts on its edge, direction decided by the + // other channel's level. + if (pcnt_channel_set_edge_action(chan_a, PCNT_CHANNEL_EDGE_ACTION_DECREASE, PCNT_CHANNEL_EDGE_ACTION_INCREASE) != ESP_OK || + pcnt_channel_set_edge_action(chan_b, PCNT_CHANNEL_EDGE_ACTION_INCREASE, PCNT_CHANNEL_EDGE_ACTION_DECREASE) != ESP_OK) { + LOG_E(TAG, "Pulse counter edge action config failed"); + pcnt_del_unit(unit); + return ERROR_RESOURCE; + } + if (pcnt_channel_set_level_action(chan_a, PCNT_CHANNEL_LEVEL_ACTION_KEEP, PCNT_CHANNEL_LEVEL_ACTION_INVERSE) != ESP_OK || + pcnt_channel_set_level_action(chan_b, PCNT_CHANNEL_LEVEL_ACTION_KEEP, PCNT_CHANNEL_LEVEL_ACTION_INVERSE) != ESP_OK) { + LOG_E(TAG, "Pulse counter level action config failed"); + pcnt_del_unit(unit); + return ERROR_RESOURCE; + } + + if (pcnt_unit_add_watch_point(unit, PCNT_LOW_LIMIT) != ESP_OK || + pcnt_unit_add_watch_point(unit, PCNT_HIGH_LIMIT) != ESP_OK) { + LOG_E(TAG, "Pulse counter watch point config failed"); + pcnt_del_unit(unit); + return ERROR_RESOURCE; + } + + if (pcnt_unit_enable(unit) != ESP_OK || + pcnt_unit_clear_count(unit) != ESP_OK || + pcnt_unit_start(unit) != ESP_OK) { + LOG_E(TAG, "Pulse counter could not be started"); + pcnt_del_unit(unit); + return ERROR_RESOURCE; + } + + *out_unit = unit; + return ERROR_NONE; +} + +static error_t start(Device* device) { + const auto* config = GET_CONFIG(device); + + auto* internal = new (std::nothrow) TpagerEncoderInternal(); + if (internal == nullptr) { + return ERROR_OUT_OF_MEMORY; + } + + error_t error = init_pcnt_unit(config, &internal->pcnt_unit); + if (error != ERROR_NONE) { + delete internal; + return error; + } + + internal->pin_enter = gpio_descriptor_acquire(config->pin_enter.gpio_controller, config->pin_enter.pin, GPIO_OWNER_GPIO); + if (internal->pin_enter == nullptr) { + pcnt_unit_stop(internal->pcnt_unit); + pcnt_del_unit(internal->pcnt_unit); + delete internal; + return ERROR_RESOURCE; + } + + error = gpio_descriptor_set_flags(internal->pin_enter, GPIO_FLAG_DIRECTION_INPUT); + if (error != ERROR_NONE) { + gpio_descriptor_release(internal->pin_enter); + pcnt_unit_stop(internal->pcnt_unit); + pcnt_del_unit(internal->pcnt_unit); + delete internal; + return error; + } + + device_set_driver_data(device, internal); + return ERROR_NONE; +} + +static error_t stop(Device* device) { + auto* internal = GET_INTERNAL(device); + + gpio_descriptor_release(internal->pin_enter); + + if (pcnt_unit_stop(internal->pcnt_unit) != ESP_OK) { + LOG_W(TAG, "Failed to stop encoder"); + } + if (pcnt_del_unit(internal->pcnt_unit) != ESP_OK) { + LOG_W(TAG, "Failed to delete encoder"); + } + + device_set_driver_data(device, nullptr); + delete internal; + return ERROR_NONE; +} + +// endregion + +static constexpr TpagerEncoderApi TPAGER_ENCODER_API = { + .read_delta = read_delta, + .get_button_pressed = get_button_pressed, +}; + +const struct DeviceType TPAGER_ENCODER_TYPE { + .name = "tpager-encoder" +}; + +extern Module lilygo_module; + +Driver tpager_encoder_driver = { + .name = "tpager_encoder", + .compatible = (const char*[]) { "lilygo,tpager-encoder", nullptr }, + .start_device = start, + .stop_device = stop, + .api = &TPAGER_ENCODER_API, + .device_type = &TPAGER_ENCODER_TYPE, + .owner = &lilygo_module, + .internal = nullptr +}; + +} diff --git a/Drivers/lilygo-module/source/tpager_encoder_input.cpp b/Drivers/lilygo-module/source/tpager_encoder_input.cpp new file mode 100644 index 000000000..f8a5236b4 --- /dev/null +++ b/Drivers/lilygo-module/source/tpager_encoder_input.cpp @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include + +#include +#include + +constexpr auto* TAG = "tpager_encoder"; + +namespace tpager_encoder { + +static lv_indev_t* g_indev = nullptr; +static Device* g_device = nullptr; + +// Ported from the deprecated HAL's TpagerEncoder::readCallback(). g_raw_total reconstructs the +// old absolute PCNT counter value (the kernel driver's read_delta() consumes/resets on every +// call instead of accumulating forever), so the hysteresis below behaves identically: only a +// run of more than pulses_click raw pulses commits to a detent, and any remainder is discarded +// rather than carried into the next read (matches the original's pulses_prev = pulses jump). +static void read_cb(lv_indev_t*, lv_indev_data_t* data) { + constexpr int32_t pulses_click = 4; + static int32_t raw_total = 0; + static int32_t committed_total = 0; + + constexpr int enter_filter_threshold = 2; + static int enter_filter = 0; + + data->enc_diff = 0; + data->state = LV_INDEV_STATE_RELEASED; + + int32_t delta = 0; + tpager_encoder_read_delta(g_device, &delta); + raw_total += delta; + + int32_t pulse_diff = raw_total - committed_total; + if (pulse_diff > pulses_click || pulse_diff < -pulses_click) { + data->enc_diff = static_cast(pulse_diff / pulses_click); + committed_total = raw_total; + } + + bool pressed = false; + tpager_encoder_get_button_pressed(g_device, &pressed); + if (pressed && enter_filter < enter_filter_threshold) { + enter_filter++; + } + if (!pressed && enter_filter > 0) { + enter_filter--; + } + + if (enter_filter == enter_filter_threshold) { + data->state = LV_INDEV_STATE_PRESSED; + } +} + +lv_indev_t* init() { + if (g_indev != nullptr) { + LOG_W(TAG, "Already initialized"); + return g_indev; + } + + if (device_get_first_active_by_type(&TPAGER_ENCODER_TYPE, &g_device) != ERROR_NONE) { + LOG_E(TAG, "tpager_encoder kernel device not found or not started"); + return nullptr; + } + + g_indev = lv_indev_create(); + if (g_indev == nullptr) { + LOG_E(TAG, "Failed to register LVGL input device"); + device_put(g_device); + g_device = nullptr; + return nullptr; + } + + lv_indev_set_type(g_indev, LV_INDEV_TYPE_ENCODER); + lv_indev_set_read_cb(g_indev, read_cb); + LOG_I(TAG, "Initialized"); + + return g_indev; +} + +void deinit() { + if (g_indev == nullptr) { + return; + } + + lv_indev_delete(g_indev); + g_indev = nullptr; + + device_put(g_device); + g_device = nullptr; + + LOG_I(TAG, "Deinitialized"); +} + +} diff --git a/Drivers/ssd1306-module/CMakeLists.txt b/Drivers/ssd1306-module/CMakeLists.txt new file mode 100644 index 000000000..3f3261b6e --- /dev/null +++ b/Drivers/ssd1306-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(ssd1306-module + SRCS ${SOURCE_FILES} + INCLUDE_DIRS include/ + REQUIRES TactilityKernel platform-esp32 esp_lcd driver +) diff --git a/Drivers/ssd1306-module/LICENSE-Apache-2.0.md b/Drivers/ssd1306-module/LICENSE-Apache-2.0.md new file mode 100644 index 000000000..f5f4b8b5e --- /dev/null +++ b/Drivers/ssd1306-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/ssd1306-module/bindings/solomon,ssd1306.yaml b/Drivers/ssd1306-module/bindings/solomon,ssd1306.yaml new file mode 100644 index 000000000..86c403614 --- /dev/null +++ b/Drivers/ssd1306-module/bindings/solomon,ssd1306.yaml @@ -0,0 +1,52 @@ +description: Solomon Systech SSD1306 monochrome OLED display panel (I2C interface) + +include: ["i2c-device.yaml"] + +compatible: "solomon,ssd1306" + +bus: i2c + +properties: + horizontal-resolution: + type: int + required: true + description: Horizontal resolution in pixels (typically 128) + vertical-resolution: + type: int + required: true + description: Vertical resolution in pixels (typically 32 or 64) + invert-color: + type: boolean + default: false + description: Invert the panel's color output + 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 + power-on-delay-ms: + type: int + default: 0 + description: > + Extra delay before reset/init, in milliseconds. Useful when the panel's power rail is + gated by a gpio-hog node: devicetree devices (including gpio-hog) are constructed and + started during kernel_init(), with no built-in settle time between one device starting + and the next, so a panel whose power was only just enabled a moment earlier may need this + to let its rail stabilize before communication starts. 0 (the default) adds no delay. + init-sequence: + type: array + element-type: uint8_t + description: > + Custom vendor bring-up sequence: a flat list of command bytes, each sent as its own + single-byte SSD1306 command (some standalone, some acting as a parameter to the byte + before them - this chip's protocol doesn't distinguish the two at the wire level, and + neither does this encoding). Omit to use the SSD1306 component's own generic + esp_lcd_panel_init() sequence instead - note that some panels are known not to + initialize correctly through it (see e.g. the Heltec WiFi LoRa 32 V3's own required + override), so most boards should supply their own sequence here. + +# No 'backlight' property: SSD1306 is a self-emitting OLED panel with no backlight to control, +# unlike this repo's other display bindings. diff --git a/Drivers/ssd1306-module/devicetree.yaml b/Drivers/ssd1306-module/devicetree.yaml new file mode 100644 index 000000000..a07d6f334 --- /dev/null +++ b/Drivers/ssd1306-module/devicetree.yaml @@ -0,0 +1,3 @@ +dependencies: + - TactilityKernel +bindings: bindings diff --git a/Drivers/ssd1306-module/include/bindings/ssd1306.h b/Drivers/ssd1306-module/include/bindings/ssd1306.h new file mode 100644 index 000000000..56972877c --- /dev/null +++ b/Drivers/ssd1306-module/include/bindings/ssd1306.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. "solomon,ssd1306" -> ssd1306_config_dt), not from the node name or +// driver name, so the tag here must match that exactly. +DEFINE_DEVICETREE(ssd1306, struct Ssd1306Config) diff --git a/Drivers/ssd1306-module/include/drivers/ssd1306.h b/Drivers/ssd1306-module/include/drivers/ssd1306.h new file mode 100644 index 000000000..d9d694717 --- /dev/null +++ b/Drivers/ssd1306-module/include/drivers/ssd1306.h @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include +#include + +struct Ssd1306Config { + // Devicetree address hint - unlike this repo's other I2C touch chips (which sit at a fixed + // address), the SSD1306's address genuinely varies by hardware strap (0x3C or 0x3D), so this + // is actually used, not just documentation. + uint8_t address; + uint16_t horizontal_resolution; + uint16_t vertical_resolution; + bool invert_color; + + // Reset pin. GPIO_PIN_SPEC_NONE skips the reset toggle entirely. + struct GpioPinSpec pin_reset; + bool reset_active_high; + + // See the 'power-on-delay-ms' binding property. + uint32_t power_on_delay_ms; + + // Custom vendor init sequence: a flat list of command bytes, each sent as its own + // single-byte SSD1306 command. NULL/0 falls back to the standard esp_lcd_panel_init(). + const uint8_t* init_sequence; + uint32_t init_sequence_length; +}; + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/ssd1306-module/include/ssd1306_module.h b/Drivers/ssd1306-module/include/ssd1306_module.h new file mode 100644 index 000000000..2d7b180a3 --- /dev/null +++ b/Drivers/ssd1306-module/include/ssd1306_module.h @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +extern struct Module ssd1306_module; + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/ssd1306-module/source/module.cpp b/Drivers/ssd1306-module/source/module.cpp new file mode 100644 index 000000000..2ab1b0b25 --- /dev/null +++ b/Drivers/ssd1306-module/source/module.cpp @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include + +extern "C" { + +extern Driver ssd1306_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(&ssd1306_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(&ssd1306_driver) == ERROR_NONE); + return ERROR_NONE; +} + +Module ssd1306_module = { + .name = "ssd1306", + .start = start, + .stop = stop, + .symbols = nullptr, + .internal = nullptr +}; + +} // extern "C" diff --git a/Drivers/ssd1306-module/source/ssd1306.cpp b/Drivers/ssd1306-module/source/ssd1306.cpp new file mode 100644 index 000000000..d7ab35ee7 --- /dev/null +++ b/Drivers/ssd1306-module/source/ssd1306.cpp @@ -0,0 +1,360 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include + +#define TAG "SSD1306" +#define GET_CONFIG(device) (static_cast((device)->config)) + +struct Ssd1306Internal { + esp_lcd_panel_io_handle_t io_handle; + esp_lcd_panel_handle_t panel_handle; + // Scratch buffer for the page-format transpose in ssd1306_draw_bitmap(); sized once for the + // full panel resolution and reused every draw_bitmap() call to avoid a malloc/free per frame. + uint8_t* page_buf; + size_t page_buf_size; +}; + +static gpio_num_t pin_or_nc(const struct GpioPinSpec& pin) { + return pin.gpio_controller == nullptr ? GPIO_NUM_NC : static_cast(pin.pin); +} + +// region Driver lifecycle + +static esp_err_t create_io_handle(Device* parent, uint8_t address, esp_lcd_panel_io_handle_t* out_handle) { + // dc_bit_offset=6/dc_low_on_data=false gives a 0x00 control byte before command bytes and + // 0x40 before pixel data - the standard SSD1306 I2C control-byte convention. lcd_cmd_bits=0 + // means tx_param()'s own "cmd" phase is skipped entirely (see send_command() below): every + // command byte is transmitted as if it were "data" following the correct control byte, + // rather than a separately-framed command - this chip's protocol has no such framing to begin + // with, unlike the multi-byte-per-command SPI/QSPI panels elsewhere in this repo. + esp_lcd_panel_io_i2c_config_t io_config = { + .dev_addr = address, + .on_color_trans_done = nullptr, + .user_ctx = nullptr, + .control_phase_bytes = 1, + .dc_bit_offset = 6, + .lcd_cmd_bits = 0, + .lcd_param_bits = 0, + .flags = { + .dc_low_on_data = false, + .disable_control_phase = false, + }, + .scl_speed_hz = 0, + }; + + 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; +} + +// Sends a single SSD1306 command byte: [control byte, this byte]. lcd_cmd is passed as 0 and +// ignored on the wire (see create_io_handle()'s comment) - only param/param_size actually reach +// the bus, so this doubles as the mechanism for both standalone commands (e.g. 0xAE display-off) +// and command+parameter pairs (each byte of which is just its own call to this function, matching +// how the original hand-written driver issued them as separate I2C writes). +static bool send_command(esp_lcd_panel_io_handle_t io, uint8_t command) { + return esp_lcd_panel_io_tx_param(io, 0, &command, 1) == ESP_OK; +} + +static bool send_init_sequence(esp_lcd_panel_io_handle_t io, const uint8_t* bytes, uint32_t length) { + for (uint32_t i = 0; i < length; i++) { + if (!send_command(io, bytes[i])) { + LOG_E(TAG, "Failed to send init sequence byte %lu", (unsigned long)i); + return false; + } + } + return true; +} + +// Manual reset with explicit timing, rather than going through esp_lcd_panel_reset(): this +// chip/config combination is known to need a specific assert/settle timing that the panel's +// standard reset (which we intentionally never wire up - reset_gpio_num is always -1 below) may +// not otherwise match. Runs before the panel handle is even created, so a subsequent init sees a +// freshly-reset chip. +static void perform_hardware_reset(const Ssd1306Config* config) { + gpio_num_t pin = pin_or_nc(config->pin_reset); + if (pin == GPIO_NUM_NC) { + return; + } + + gpio_config_t io_conf = { + .pin_bit_mask = 1ULL << pin, + .mode = GPIO_MODE_OUTPUT, + .pull_up_en = GPIO_PULLUP_DISABLE, + .pull_down_en = GPIO_PULLDOWN_DISABLE, + .intr_type = GPIO_INTR_DISABLE, + }; + gpio_config(&io_conf); + + gpio_set_level(pin, config->reset_active_high ? 1 : 0); + vTaskDelay(pdMS_TO_TICKS(10)); + gpio_set_level(pin, config->reset_active_high ? 0 : 1); + vTaskDelay(pdMS_TO_TICKS(100)); +} + +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(Ssd1306Internal))); + if (internal == nullptr) { + return ERROR_OUT_OF_MEMORY; + } + + internal->page_buf_size = (size_t)config->horizontal_resolution * ((config->vertical_resolution + 7) / 8); + internal->page_buf = static_cast(malloc(internal->page_buf_size)); + if (internal->page_buf == nullptr) { + free(internal); + return ERROR_OUT_OF_MEMORY; + } + + // Lets a power rail gated by a gpio-hog node (asserted moments earlier, during the same + // kernel_init() pass) stabilize before this device tries to reset/talk to the chip. See the + // 'power-on-delay-ms' binding property. + if (config->power_on_delay_ms > 0) { + vTaskDelay(pdMS_TO_TICKS(config->power_on_delay_ms)); + } + + if (create_io_handle(parent, config->address, &internal->io_handle) != ESP_OK) { + LOG_E(TAG, "Failed to create panel IO"); + free(internal->page_buf); + free(internal); + return ERROR_RESOURCE; + } + + perform_hardware_reset(config); + + esp_lcd_panel_ssd1306_config_t ssd1306_config = { + .height = static_cast(config->vertical_resolution), + }; + + // color_space (not rgb_ele_order): this deprecated union member is the only one whose type + // can express ESP_LCD_COLOR_SPACE_MONOCHROME - rgb_ele_order's own enum only covers RGB/BGR. + esp_lcd_panel_dev_config_t panel_config = { + .reset_gpio_num = -1, // always -1: reset is handled manually above, not by the panel itself + .color_space = ESP_LCD_COLOR_SPACE_MONOCHROME, + .data_endian = LCD_RGB_DATA_ENDIAN_BIG, + .bits_per_pixel = 1, + .flags = { .reset_active_high = false }, + .vendor_config = &ssd1306_config, + }; + + if (esp_lcd_new_panel_ssd1306(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); + free(internal->page_buf); + free(internal); + return ERROR_RESOURCE; + } + + // Bring-up sequence. Every failure path below 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; + if (config->init_sequence != nullptr && config->init_sequence_length > 0) { + ok = send_init_sequence(internal->io_handle, config->init_sequence, config->init_sequence_length); + } else { + ok = esp_lcd_panel_init(internal->panel_handle) == 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); + free(internal->page_buf); + free(internal); + return ERROR_RESOURCE; + } + + vTaskDelay(pdMS_TO_TICKS(100)); // Let the panel stabilize before the first real draw_bitmap() + + 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; + } + + free(internal->page_buf); + free(internal); + device_set_driver_data(device, nullptr); + return ERROR_NONE; +} + +// endregion + +// region DisplayApi + +static error_t ssd1306_reset(Device* device) { + perform_hardware_reset(GET_CONFIG(device)); + return ERROR_NONE; +} + +static error_t ssd1306_init(Device* device) { + auto* internal = static_cast(device_get_driver_data(device)); + const auto* config = GET_CONFIG(device); + bool ok; + if (config->init_sequence != nullptr && config->init_sequence_length > 0) { + ok = send_init_sequence(internal->io_handle, config->init_sequence, config->init_sequence_length); + } else { + ok = esp_lcd_panel_init(internal->panel_handle) == ESP_OK; + } + return ok ? ERROR_NONE : ERROR_RESOURCE; +} + +// DISPLAY_COLOR_FORMAT_MONOCHROME's wire format (matching LVGL's LV_COLOR_FORMAT_I1, palette +// stripped) is row-major: each byte packs 8 horizontally-adjacent pixels, MSB = leftmost. The +// SSD1306's GDDRAM is page-addressed instead: each byte packs 8 *vertically* adjacent pixels of +// one column, LSB = topmost row of the page. Transpose one into the other before handing off to +// the vendor component, which expects page format directly. +static void transpose_row_major_to_pages(const uint8_t* src, int32_t width, int32_t height, uint8_t* out) { + uint32_t row_stride = (uint32_t)(width + 7) / 8; + for (int32_t y = 0; y < height; y++) { + uint8_t page = (uint8_t)(y / 8); + uint8_t bit_in_page = (uint8_t)(y % 8); + for (int32_t x = 0; x < width; x++) { + uint8_t src_byte = src[y * row_stride + x / 8]; + uint8_t pixel = (src_byte >> (7 - (x % 8))) & 0x01; + out[page * width + x] |= (uint8_t)(pixel << bit_in_page); + } + } +} + +static error_t ssd1306_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)); + int32_t width = x_end - x_start; + int32_t height = y_end - y_start; + size_t needed = (size_t)width * ((height + 7) / 8); + check(needed <= internal->page_buf_size); + + memset(internal->page_buf, 0, needed); + transpose_row_major_to_pages(static_cast(color_data), width, height, internal->page_buf); + + return esp_lcd_panel_draw_bitmap(internal->panel_handle, x_start, y_start, x_end, y_end, internal->page_buf) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE; +} + +// mirror/swap_xy/set_gap are not exposed: the SSD1306 vendor component doesn't implement them. + +static error_t ssd1306_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 ssd1306_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: the SSD1306 vendor component doesn't implement it either (display +// on/off via 0xAE/0xAF is this chip's equivalent, already covered by disp_on_off above). + +static enum DisplayColorFormat ssd1306_get_color_format(Device*) { + return DISPLAY_COLOR_FORMAT_MONOCHROME; +} + +static uint16_t ssd1306_get_resolution_x(Device* device) { + return GET_CONFIG(device)->horizontal_resolution; +} + +static uint16_t ssd1306_get_resolution_y(Device* device) { + return GET_CONFIG(device)->vertical_resolution; +} + +static void ssd1306_get_frame_buffer(Device*, uint8_t, void** out_buffer) { + *out_buffer = nullptr; +} + +static uint8_t ssd1306_get_frame_buffer_count(Device*) { + return 0; +} + +// get_backlight is not exposed: SSD1306 is a self-emitting OLED panel with no backlight. + +// endregion + +static const DisplayApi ssd1306_display_api = { + .capabilities = DISPLAY_CAPABILITY_INVERT_COLOR | DISPLAY_CAPABILITY_ON_OFF, + .reset = ssd1306_reset, + .init = ssd1306_init, + .draw_bitmap = ssd1306_draw_bitmap, + .mirror = nullptr, + .swap_xy = nullptr, + .get_swap_xy = nullptr, + .get_mirror_x = nullptr, + .get_mirror_y = nullptr, + .set_gap = nullptr, + .invert_color = ssd1306_invert_color, + .disp_on_off = ssd1306_disp_on_off, + .disp_sleep = nullptr, + .get_color_format = ssd1306_get_color_format, + .get_resolution_x = ssd1306_get_resolution_x, + .get_resolution_y = ssd1306_get_resolution_y, + .get_frame_buffer = ssd1306_get_frame_buffer, + .get_frame_buffer_count = ssd1306_get_frame_buffer_count, + .get_backlight = nullptr, + .has_capability = nullptr, +}; + +Driver ssd1306_driver = { + .name = "ssd1306", + .compatible = (const char*[]) { "solomon,ssd1306", nullptr }, + .start_device = start, + .stop_device = stop, + .api = &ssd1306_display_api, + .device_type = &DISPLAY_TYPE, + .owner = &ssd1306_module, + .internal = nullptr +}; diff --git a/Drivers/st7701-module/CMakeLists.txt b/Drivers/st7701-module/CMakeLists.txt new file mode 100644 index 000000000..d3c19e8bf --- /dev/null +++ b/Drivers/st7701-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(st7701-module + SRCS ${SOURCE_FILES} + INCLUDE_DIRS include/ + REQUIRES TactilityKernel platform-esp32 esp_lcd_st7701 esp_lcd_panel_io_additions esp_lcd driver +) diff --git a/Drivers/st7701-module/LICENSE-Apache-2.0.md b/Drivers/st7701-module/LICENSE-Apache-2.0.md new file mode 100644 index 000000000..f5f4b8b5e --- /dev/null +++ b/Drivers/st7701-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/st7701-module/bindings/sitronix,st7701.yaml b/Drivers/st7701-module/bindings/sitronix,st7701.yaml new file mode 100644 index 000000000..b2db4d518 --- /dev/null +++ b/Drivers/st7701-module/bindings/sitronix,st7701.yaml @@ -0,0 +1,263 @@ +description: > + Sitronix ST7701 display panel, driven over ESP-IDF's esp_lcd_st7701 component. Like + espressif,esp32-rgb-display, pixel data goes out over the RGB (parallel timing-driven) + interface, but the panel also has its own command interface: a bit-banged 3-wire SPI bus + (CS/SCL/SDA over plain GPIOs) used to push the vendor bring-up sequence and, optionally, + runtime commands (mirror, display on/off). + +compatible: "sitronix,st7701" + +properties: + horizontal-resolution: + type: int + required: true + description: Horizontal resolution in pixels + vertical-resolution: + type: int + required: true + description: Vertical resolution in pixels + pixel-clock-hz: + type: int + required: true + description: Pixel clock frequency in Hz + 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 + hsync-idle-low: + type: boolean + default: false + description: The hsync signal is low in the idle state + vsync-idle-low: + type: boolean + default: false + description: The vsync signal is low in the idle state + de-idle-high: + type: boolean + default: false + description: The DE signal is high in the idle state + pclk-active-neg: + type: boolean + default: false + description: Whether display data is clocked out on the falling edge of PCLK + pclk-idle-high: + type: boolean + default: false + description: The PCLK signal stays high in the idle phase + data-width: + type: int + default: 16 + description: Number of parallel data lines wired (8 or 16) + bits-per-pixel: + type: int + default: 16 + description: Color depth in bits per pixel (16, 18 or 24 - the ST7701 has no "match data-width" default) + num-fbs: + type: int + default: 1 + description: Number of screen-sized frame buffers to allocate (0 or 1 = single-buffered) + bounce-buffer-size-px: + type: int + default: 0 + description: Non-zero enables the DRAM bounce-buffer DMA path, sized in pixels + sram-trans-align: + type: int + default: 8 + description: Alignment of buffers allocated in internal SRAM + psram-trans-align: + type: int + default: 64 + description: Alignment of buffers allocated in PSRAM + pin-hsync: + type: phandles + default: GPIO_PIN_SPEC_NONE + description: HSYNC GPIO pin, optional for panels that only need DE (data-enable) sync + pin-vsync: + type: phandles + default: GPIO_PIN_SPEC_NONE + description: VSYNC GPIO pin, optional for panels that only need DE (data-enable) sync + pin-de: + type: phandles + default: GPIO_PIN_SPEC_NONE + description: Data-enable GPIO pin, optional + pin-pclk: + type: phandles + required: true + description: Pixel-clock GPIO pin + pin-disp: + type: phandles + default: GPIO_PIN_SPEC_NONE + description: Optional display-enable control GPIO pin + pin-data0: + type: phandles + default: GPIO_PIN_SPEC_NONE + description: Data bus bit 0 GPIO pin + pin-data1: + type: phandles + default: GPIO_PIN_SPEC_NONE + description: Data bus bit 1 GPIO pin + pin-data2: + type: phandles + default: GPIO_PIN_SPEC_NONE + description: Data bus bit 2 GPIO pin + pin-data3: + type: phandles + default: GPIO_PIN_SPEC_NONE + description: Data bus bit 3 GPIO pin + pin-data4: + type: phandles + default: GPIO_PIN_SPEC_NONE + description: Data bus bit 4 GPIO pin + pin-data5: + type: phandles + default: GPIO_PIN_SPEC_NONE + description: Data bus bit 5 GPIO pin + pin-data6: + type: phandles + default: GPIO_PIN_SPEC_NONE + description: Data bus bit 6 GPIO pin + pin-data7: + type: phandles + default: GPIO_PIN_SPEC_NONE + description: Data bus bit 7 GPIO pin + pin-data8: + type: phandles + default: GPIO_PIN_SPEC_NONE + description: Data bus bit 8 GPIO pin + pin-data9: + type: phandles + default: GPIO_PIN_SPEC_NONE + description: Data bus bit 9 GPIO pin + pin-data10: + type: phandles + default: GPIO_PIN_SPEC_NONE + description: Data bus bit 10 GPIO pin + pin-data11: + type: phandles + default: GPIO_PIN_SPEC_NONE + description: Data bus bit 11 GPIO pin + pin-data12: + type: phandles + default: GPIO_PIN_SPEC_NONE + description: Data bus bit 12 GPIO pin + pin-data13: + type: phandles + default: GPIO_PIN_SPEC_NONE + description: Data bus bit 13 GPIO pin + pin-data14: + type: phandles + default: GPIO_PIN_SPEC_NONE + description: Data bus bit 14 GPIO pin + pin-data15: + type: phandles + default: GPIO_PIN_SPEC_NONE + description: Data bus bit 15 GPIO pin + disp-active-low: + type: boolean + default: false + description: A low level on the display-enable control signal turns the screen on + refresh-on-demand: + type: boolean + default: false + description: Only refresh the frame buffer on explicit draw_bitmap calls + fb-in-psram: + type: boolean + default: true + description: Allocate the frame buffer from PSRAM, preferentially + double-fb: + type: boolean + default: false + description: Allocate two screen-sized frame buffers (equivalent to num-fbs = 2) + no-fb: + type: boolean + default: false + description: Don't allocate a frame buffer; bounce buffer must be filled manually + bb-invalidate-cache: + type: boolean + default: false + description: Invalidate cache on bounce-buffer reads (can be dangerous with multi-core writers) + 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 + pin-cs: + type: phandles + required: true + description: Chip-select GPIO pin for the bit-banged 3-wire command bus + pin-scl: + type: phandles + required: true + description: Clock GPIO pin for the bit-banged 3-wire command bus + pin-sda: + type: phandles + required: true + description: Data GPIO pin for the bit-banged 3-wire command bus (write-only, no MISO) + scl-active-edge: + type: boolean + default: false + description: Sample data on the falling edge of SCL instead of the rising edge + pin-reset: + type: phandles + default: GPIO_PIN_SPEC_NONE + description: Optional hardware reset pin for the panel. Falls back to a software reset + (sent over the 3-wire bus) if not set. + reset-active-high: + type: boolean + default: false + description: Whether the reset pin is active high + mirror-by-cmd: + type: boolean + default: false + description: Implement mirror() by sending an LCD command instead of flipping the RGB + peripheral's scan direction in software + auto-del-panel-io: + type: boolean + default: false + description: Release the 3-wire command bus (and its pins) right after bring-up. Only + useful when those pins are physically shared with the RGB data/sync pins; leave false + if the 3-wire bus has its own dedicated pins, since runtime commands (mirror-by-cmd, + disp-on-off without a dedicated pin-disp) need the bus to stay alive. + 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 = [0xFF 5 0 0x77 0x01 0x00 0x00 0x10 0x11 0 120 0x29 0 0];`. + Omit to use the ST7701 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/st7701-module/devicetree.yaml b/Drivers/st7701-module/devicetree.yaml new file mode 100644 index 000000000..a07d6f334 --- /dev/null +++ b/Drivers/st7701-module/devicetree.yaml @@ -0,0 +1,3 @@ +dependencies: + - TactilityKernel +bindings: bindings diff --git a/Drivers/st7701-module/include/bindings/st7701.h b/Drivers/st7701-module/include/bindings/st7701.h new file mode 100644 index 000000000..d708431a6 --- /dev/null +++ b/Drivers/st7701-module/include/bindings/st7701.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,st7701" -> st7701_config_dt), not from the node name or +// driver name, so the tag here must match that exactly. +DEFINE_DEVICETREE(st7701, struct St7701Config) diff --git a/Drivers/st7701-module/include/drivers/st7701.h b/Drivers/st7701-module/include/drivers/st7701.h new file mode 100644 index 000000000..834a1a950 --- /dev/null +++ b/Drivers/st7701-module/include/drivers/st7701.h @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include +#include + +struct St7701Config { + uint16_t horizontal_resolution; + uint16_t vertical_resolution; + + uint32_t pixel_clock_hz; + 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; + bool hsync_idle_low; + bool vsync_idle_low; + bool de_idle_high; + bool pclk_active_neg; + bool pclk_idle_high; + + // Number of parallel data lines actually wired (8 or 16). Only the first data_width + // pin-dataN fields below are used; the rest are ignored. + uint8_t data_width; + // Color depth in bpp (16, 18 or 24). Passed to both the RGB timing config and the ST7701 + // vendor config, which uses it to pick its MADCTL/COLMOD bring-up values. + uint8_t bits_per_pixel; + // Number of screen-sized frame buffers the driver allocates (0 or 1 = single-buffered). + uint8_t num_fbs; + // Non-zero enables the DRAM bounce-buffer DMA path, sized in pixels. + uint32_t bounce_buffer_size_px; + uint32_t sram_trans_align; + uint32_t psram_trans_align; + + struct GpioPinSpec pin_hsync; + struct GpioPinSpec pin_vsync; + struct GpioPinSpec pin_de; + struct GpioPinSpec pin_pclk; + // Optional display-enable control pin, GPIO_PIN_SPEC_NONE if unused. + struct GpioPinSpec pin_disp; + + struct GpioPinSpec pin_data0; + struct GpioPinSpec pin_data1; + struct GpioPinSpec pin_data2; + struct GpioPinSpec pin_data3; + struct GpioPinSpec pin_data4; + struct GpioPinSpec pin_data5; + struct GpioPinSpec pin_data6; + struct GpioPinSpec pin_data7; + struct GpioPinSpec pin_data8; + struct GpioPinSpec pin_data9; + struct GpioPinSpec pin_data10; + struct GpioPinSpec pin_data11; + struct GpioPinSpec pin_data12; + struct GpioPinSpec pin_data13; + struct GpioPinSpec pin_data14; + struct GpioPinSpec pin_data15; + + bool disp_active_low; + bool refresh_on_demand; + bool fb_in_psram; + bool double_fb; + bool no_fb; + bool bb_invalidate_cache; + + bool swap_xy; + bool mirror_x; + bool mirror_y; + bool invert_color; + + // Bit-banged 3-wire command bus (CS/SCL/SDA over plain GPIOs) used to push the vendor + // bring-up sequence and, depending on flags below, runtime commands. Separate from the + // RGB pixel bus above - the ST7701 has no MISO line in 3-wire mode. + struct GpioPinSpec pin_cs; + struct GpioPinSpec pin_scl; + struct GpioPinSpec pin_sda; + bool scl_active_edge; + + // Optional hardware reset pin for the panel. GPIO_PIN_SPEC_NONE falls back to a software + // reset sent over the 3-wire bus (see esp_lcd_st7701's panel_st7701_reset()). + struct GpioPinSpec pin_reset; + bool reset_active_high; + + // See the 'mirror-by-cmd' and 'auto-del-panel-io' binding properties. + bool mirror_by_cmd; + bool auto_del_panel_io; + + // 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 + // ST7701 component's own built-in default sequence (see vendor_specific_init_default in + // esp_lcd_st7701_rgb.c) - not guaranteed to match any particular panel's actual bring-up + // requirements, so most boards should supply their own via this property. + 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/st7701-module/include/st7701_module.h b/Drivers/st7701-module/include/st7701_module.h new file mode 100644 index 000000000..3cb852826 --- /dev/null +++ b/Drivers/st7701-module/include/st7701_module.h @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +extern struct Module st7701_module; + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/st7701-module/source/module.cpp b/Drivers/st7701-module/source/module.cpp new file mode 100644 index 000000000..80c1e5cde --- /dev/null +++ b/Drivers/st7701-module/source/module.cpp @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include + +extern "C" { + +extern Driver st7701_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(&st7701_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(&st7701_driver) == ERROR_NONE); + return ERROR_NONE; +} + +Module st7701_module = { + .name = "st7701", + .start = start, + .stop = stop, + .symbols = nullptr, + .internal = nullptr +}; + +} // extern "C" diff --git a/Drivers/st7701-module/source/st7701.cpp b/Drivers/st7701-module/source/st7701.cpp new file mode 100644 index 000000000..83c3e5f7e --- /dev/null +++ b/Drivers/st7701-module/source/st7701.cpp @@ -0,0 +1,543 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#if SOC_LCD_RGB_SUPPORTED + +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +#define TAG "St7701" +#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 St7701Internal { + 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_frame_buf_complete once per real DMA scan-out of a whole frame. Only + // waited on in draw_bitmap() when color_data is one of frame_buffers - see the comment there. + 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(). + st7701_lcd_init_cmd_t* parsed_init_cmds; +}; + +// esp_lcd_rgb_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_rgb_panel_get_frame_buffer()): it just repoints which +// buffer is scanned out and returns almost instantly - well before the RGB peripheral's DMA 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_frame_buf_complete fires once per +// actual whole-frame DMA 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. +static bool IRAM_ATTR on_frame_buf_complete(esp_lcd_panel_handle_t, const esp_lcd_rgb_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); +} + +// 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,st7701.yaml) into a heap-allocated st7701_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, st7701_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(st7701_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(St7701Internal))); + if (internal == nullptr) { + return ERROR_OUT_OF_MEMORY; + } + internal->parsed_init_cmds = nullptr; + + const st7701_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; + } + + spi_line_config_t line_config = { + .cs_io_type = IO_TYPE_GPIO, + .cs_gpio_num = pin_or_unused(config->pin_cs), + .scl_io_type = IO_TYPE_GPIO, + .scl_gpio_num = pin_or_unused(config->pin_scl), + .sda_io_type = IO_TYPE_GPIO, + .sda_gpio_num = pin_or_unused(config->pin_sda), + .io_expander = nullptr, + }; + // scl_active_edge is a runtime bool, not a compile-time constant, so it can't be passed + // through the macro's brace-init `.spi_mode = scl_active_edge ? 1 : 0` directly - that trips + // -Werror=narrowing on the bitfield (only literal 0/1 args avoid it). Assign it afterward + // instead, as a plain (non-narrowing) assignment. + esp_lcd_panel_io_3wire_spi_config_t io_config = ST7701_PANEL_IO_3WIRE_SPI_CONFIG(line_config, 0); + io_config.spi_mode = config->scl_active_edge ? 1 : 0; + + esp_err_t ret = esp_lcd_new_panel_io_3wire_spi(&io_config, &internal->io_handle); + if (ret != ESP_OK) { + LOG_E(TAG, "Failed to create panel IO: %s", esp_err_to_name(ret)); + free(internal->parsed_init_cmds); + free(internal); + return ERROR_RESOURCE; + } + + esp_lcd_rgb_panel_config_t rgb_config = { + .clk_src = LCD_CLK_SRC_DEFAULT, + .timings = { + .pclk_hz = config->pixel_clock_hz, + .h_res = config->horizontal_resolution, + .v_res = 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 = { + .hsync_idle_low = config->hsync_idle_low, + .vsync_idle_low = config->vsync_idle_low, + .de_idle_high = config->de_idle_high, + .pclk_active_neg = config->pclk_active_neg, + .pclk_idle_high = config->pclk_idle_high, + } + }, + .data_width = config->data_width, + .bits_per_pixel = config->bits_per_pixel, + .num_fbs = config->num_fbs, + .bounce_buffer_size_px = config->bounce_buffer_size_px, + .sram_trans_align = config->sram_trans_align, + .psram_trans_align = config->psram_trans_align, + .hsync_gpio_num = pin_or_unused(config->pin_hsync), + .vsync_gpio_num = pin_or_unused(config->pin_vsync), + .de_gpio_num = pin_or_unused(config->pin_de), + .pclk_gpio_num = pin_or_unused(config->pin_pclk), + .disp_gpio_num = pin_or_unused(config->pin_disp), + .data_gpio_nums = { + pin_or_unused(config->pin_data0), + pin_or_unused(config->pin_data1), + pin_or_unused(config->pin_data2), + pin_or_unused(config->pin_data3), + pin_or_unused(config->pin_data4), + pin_or_unused(config->pin_data5), + pin_or_unused(config->pin_data6), + pin_or_unused(config->pin_data7), + pin_or_unused(config->pin_data8), + pin_or_unused(config->pin_data9), + pin_or_unused(config->pin_data10), + pin_or_unused(config->pin_data11), + pin_or_unused(config->pin_data12), + pin_or_unused(config->pin_data13), + pin_or_unused(config->pin_data14), + pin_or_unused(config->pin_data15), + }, + .flags = { + .disp_active_low = config->disp_active_low, + .refresh_on_demand = config->refresh_on_demand, + .fb_in_psram = config->fb_in_psram, + .double_fb = config->double_fb, + .no_fb = config->no_fb, + .bb_invalidate_cache = config->bb_invalidate_cache, + } + }; + + // This Config struct only exposes 16 named data pins, so on chips whose RGB peripheral has + // more data lines than that, the tail of the array must be explicitly marked unused rather + // than left as the aggregate-init default of 0 (which would look like "GPIO0 is wired here"). + for (size_t i = 16; i < sizeof(rgb_config.data_gpio_nums) / sizeof(rgb_config.data_gpio_nums[0]); i++) { + rgb_config.data_gpio_nums[i] = -1; + } + + st7701_vendor_config_t vendor_config = { + .init_cmds = init_cmds, + .init_cmds_size = init_cmds_size, + .rgb_config = &rgb_config, + .flags = { + .use_mipi_interface = 0, + .mirror_by_cmd = config->mirror_by_cmd, + .auto_del_panel_io = config->auto_del_panel_io, + }, + }; + + esp_lcd_panel_dev_config_t panel_config = { + .reset_gpio_num = pin_or_unused(config->pin_reset), + .rgb_ele_order = 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, + }; + + ret = esp_lcd_new_panel_st7701(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); + 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 3-wire bus before bringing up the underlying RGB peripheral. Every + // failure path below 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_swap_xy(internal->panel_handle, config->swap_xy) == ESP_OK && + esp_lcd_panel_mirror(internal->panel_handle, config->mirror_x, config->mirror_y) == ESP_OK && + esp_lcd_panel_invert_color(internal->panel_handle, config->invert_color) == 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); + if (!config->auto_del_panel_io) { + esp_lcd_panel_io_del(internal->io_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_rgb_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; + switch (fb_num) { + case 1: + ret = esp_lcd_rgb_panel_get_frame_buffer(internal->panel_handle, 1, &internal->frame_buffers[0]); + break; + case 2: + ret = esp_lcd_rgb_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); + if (!config->auto_del_panel_io) { + esp_lcd_panel_io_del(internal->io_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); + if (!config->auto_del_panel_io) { + esp_lcd_panel_io_del(internal->io_handle); + } + free(internal->parsed_init_cmds); + free(internal); + return ERROR_OUT_OF_MEMORY; + } + + esp_lcd_rgb_panel_event_callbacks_t callbacks = {}; + callbacks.on_frame_buf_complete = on_frame_buf_complete; + if (esp_lcd_rgb_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); + if (!config->auto_del_panel_io) { + esp_lcd_panel_io_del(internal->io_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) { + const auto* config = GET_CONFIG(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 auto_del_panel_io was set, esp_lcd_new_panel_st7701() already deleted the IO handle + // itself right after bring-up (to free pins shared with the RGB bus) - deleting it again + // here would double-free. + if (!config->auto_del_panel_io && 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->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 st7701_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 st7701_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_frame_buf_complete's comment above for why that matters) - 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 st7701_color_data_is_frame_buffer(const St7701Internal* 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 st7701_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 = st7701_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; +} + +static error_t st7701_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 st7701_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 st7701_get_swap_xy(Device* device) { + return GET_CONFIG(device)->swap_xy; +} + +static bool st7701_get_mirror_x(Device* device) { + return GET_CONFIG(device)->mirror_x; +} + +static bool st7701_get_mirror_y(Device* device) { + return GET_CONFIG(device)->mirror_y; +} + +// set_gap is not exposed: like plain RGB panels, the ST7701's RGB interface is a raw scan-out +// framebuffer with no addressable-window concept, so there's no gap to set. + +static error_t st7701_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 st7701_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_st7701's RGB variant has no sleep-mode entry point of its +// own (only init/del/reset/mirror/disp_on_off are overridden - see esp_lcd_st7701_rgb.c). + +static enum DisplayColorFormat st7701_get_color_format(Device*) { + return DISPLAY_COLOR_FORMAT_RGB565; +} + +static uint16_t st7701_get_resolution_x(Device* device) { + return GET_CONFIG(device)->horizontal_resolution; +} + +static uint16_t st7701_get_resolution_y(Device* device) { + return GET_CONFIG(device)->vertical_resolution; +} + +static void st7701_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 st7701_get_frame_buffer_count(Device* device) { + auto* internal = static_cast(device_get_driver_data(device)); + return internal->frame_buffer_count; +} + +static error_t st7701_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; +} + +constexpr uint32_t ST7701_CAPABILITIES = DISPLAY_CAPABILITY_CAP_MIRROR | DISPLAY_CAPABILITY_CAP_SWAP_XY | + DISPLAY_CAPABILITY_INVERT_COLOR | DISPLAY_CAPABILITY_ON_OFF | DISPLAY_CAPABILITY_BACKLIGHT; + +// Same reasoning as rgb_display's has_capability(): mirror()/swap_xy()'s rotate_mask is only +// applied when draw_bitmap()'s color_data is copied into the frame buffer by CPU. When +// frame_buffer_count > 0, LVGL is bound directly onto the panel's own frame buffers, so that copy +// - and with it the rotation - never happens for CAP_SWAP_XY. CAP_MIRROR is unaffected here +// because mirror_by_cmd (if set) still takes effect regardless of the frame-buffer-bound path. +static bool st7701_has_capability(Device* device, uint32_t capability) { + auto* internal = static_cast(device_get_driver_data(device)); + uint32_t capabilities = ST7701_CAPABILITIES; + if (internal->frame_buffer_count > 0 && !GET_CONFIG(device)->mirror_by_cmd) { + capabilities &= ~DISPLAY_CAPABILITY_CAP_MIRROR; + } + if (internal->frame_buffer_count > 0) { + capabilities &= ~DISPLAY_CAPABILITY_CAP_SWAP_XY; + } + return (capabilities & capability) == capability; +} + +// endregion + +static const DisplayApi st7701_display_api = { + .capabilities = ST7701_CAPABILITIES, + .reset = st7701_reset, + .init = st7701_init, + .draw_bitmap = st7701_draw_bitmap, + .mirror = st7701_mirror, + .swap_xy = st7701_swap_xy, + .get_swap_xy = st7701_get_swap_xy, + .get_mirror_x = st7701_get_mirror_x, + .get_mirror_y = st7701_get_mirror_y, + .set_gap = nullptr, + .invert_color = st7701_invert_color, + .disp_on_off = st7701_disp_on_off, + .disp_sleep = nullptr, + .get_color_format = st7701_get_color_format, + .get_resolution_x = st7701_get_resolution_x, + .get_resolution_y = st7701_get_resolution_y, + .get_frame_buffer = st7701_get_frame_buffer, + .get_frame_buffer_count = st7701_get_frame_buffer_count, + .get_backlight = st7701_get_backlight, + .has_capability = st7701_has_capability, +}; + +Driver st7701_driver = { + .name = "st7701", + .compatible = (const char*[]) { "sitronix,st7701", nullptr }, + .start_device = start, + .stop_device = stop, + .api = &st7701_display_api, + .device_type = &DISPLAY_TYPE, + .owner = &st7701_module, + .internal = nullptr +}; + +#endif // SOC_LCD_RGB_SUPPORTED diff --git a/Drivers/st7735-module/CMakeLists.txt b/Drivers/st7735-module/CMakeLists.txt new file mode 100644 index 000000000..f5dd74d43 --- /dev/null +++ b/Drivers/st7735-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(st7735-module + SRCS ${SOURCE_FILES} + INCLUDE_DIRS include/ + REQUIRES TactilityKernel platform-esp32 esp_lcd_st7735 esp_lcd driver +) diff --git a/Drivers/st7735-module/bindings/sitronix,st7735.yaml b/Drivers/st7735-module/bindings/sitronix,st7735.yaml new file mode 100644 index 000000000..f31afb2d5 --- /dev/null +++ b/Drivers/st7735-module/bindings/sitronix,st7735.yaml @@ -0,0 +1,77 @@ +description: Sitronix ST7735 display panel + +compatible: "sitronix,st7735" + +bus: spi + +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 + bits-per-pixel: + type: int + default: 16 + description: Color depth in bits per pixel + pixel-clock-hz: + type: int + default: 27000000 + 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/st7735-module/devicetree.yaml b/Drivers/st7735-module/devicetree.yaml new file mode 100644 index 000000000..a07d6f334 --- /dev/null +++ b/Drivers/st7735-module/devicetree.yaml @@ -0,0 +1,3 @@ +dependencies: + - TactilityKernel +bindings: bindings diff --git a/Drivers/st7735-module/include/bindings/st7735.h b/Drivers/st7735-module/include/bindings/st7735.h new file mode 100644 index 000000000..fc19fad37 --- /dev/null +++ b/Drivers/st7735-module/include/bindings/st7735.h @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include + +DEFINE_DEVICETREE(st7735, struct St7735Config) diff --git a/Drivers/st7735-module/include/drivers/st7735.h b/Drivers/st7735-module/include/drivers/st7735.h new file mode 100644 index 000000000..dc39cdb3a --- /dev/null +++ b/Drivers/st7735-module/include/drivers/st7735.h @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include +#include + +struct St7735Config { + 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 bits_per_pixel; + 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/st7735-module/include/st7735_module.h b/Drivers/st7735-module/include/st7735_module.h new file mode 100644 index 000000000..70a88a8a9 --- /dev/null +++ b/Drivers/st7735-module/include/st7735_module.h @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +extern struct Module st7735_module; + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/st7735-module/source/module.cpp b/Drivers/st7735-module/source/module.cpp new file mode 100644 index 000000000..c867f4b60 --- /dev/null +++ b/Drivers/st7735-module/source/module.cpp @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include + +extern "C" { + +extern Driver st7735_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(&st7735_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(&st7735_driver) == ERROR_NONE); + return ERROR_NONE; +} + +Module st7735_module = { + .name = "st7735", + .start = start, + .stop = stop, + .symbols = nullptr, + .internal = nullptr +}; + +} // extern "C" diff --git a/Drivers/st7735-module/source/st7735.cpp b/Drivers/st7735-module/source/st7735.cpp new file mode 100644 index 000000000..840e02689 --- /dev/null +++ b/Drivers/st7735-module/source/st7735.cpp @@ -0,0 +1,346 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include + +#include + +#define TAG "ST7735" + +#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 St7735Display::setGammaCurve() (Drivers/ST7735) - note the non-linear +// mapping, not index+1. +static const uint8_t GAMMA_CURVE_VALUES[4] = { 0x01, 0x04, 0x02, 0x08 }; + +struct St7735Internal { + esp_lcd_panel_io_handle_t io_handle; + esp_lcd_panel_handle_t panel_handle; + // Given from ISR context by on_color_trans_done() once a queued SPI transfer physically + // completes. draw_bitmap() blocks on this so it can honor DisplayApi's synchronous contract + // (see lvgl_display.c: the caller reuses/overwrites the color buffer as soon as draw_bitmap + // returns) - esp_lcd_panel_draw_bitmap() itself only queues the transfer and returns early. + SemaphoreHandle_t draw_done_semaphore; +}; + +// Fires for every completed SPI transaction on this IO (not just draw_bitmap's color transfers - +// bring-up commands like reset/init/gap go through the same IO), called from ISR context. +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(St7735Internal))); + 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 = 1, + .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 = config->bits_per_pixel, + .flags = { .reset_active_high = config->reset_active_high }, + .vendor_config = nullptr, + }; + + ret = esp_lcd_new_panel_st7735(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 st7789-module (proven correct on real panels). + // Every failure path below 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 && + (!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 (confirmed via + // ESP-IDF's esp_lcd_panel_st7789.c, same pattern), so gap_x/gap_y are passed through unswapped. + 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 st7735_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 st7735_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 st7735_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)); + + // Drain any stale signal left over from a prior non-draw transaction (bring-up commands like + // reset/gap also complete through on_color_trans_done), so the take() below can only be + // satisfied by this draw's own transfer completing. + 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; + } + + // Block until the SPI transfer physically completes: DisplayApi's draw_bitmap is a synchronous + // contract (see lvgl_display.c), so the caller must be able to safely reuse/overwrite + // color_data as soon as this call returns. esp_lcd_panel_draw_bitmap() only queues the + // transfer and returns once it's handed to the SPI peripheral, not once it's finished. + xSemaphoreTake(internal->draw_done_semaphore, portMAX_DELAY); + return ERROR_NONE; +} + +static error_t st7735_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 st7735_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: swap_xy()/mirror() calls made after +// start_device() (e.g. by an LVGL rotation binding) intentionally don't change what "rotation 0" means here. +static bool st7735_get_swap_xy(Device* device) { + return GET_CONFIG(device)->swap_xy; +} + +static bool st7735_get_mirror_x(Device* device) { + return GET_CONFIG(device)->mirror_x; +} + +static bool st7735_get_mirror_y(Device* device) { + return GET_CONFIG(device)->mirror_y; +} + +static error_t st7735_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; +} + +// Reads the devicetree-configured baseline, not live hardware state - see DisplayApi::get_gap_x(). +static int32_t st7735_get_gap_x(Device* device) { + return GET_CONFIG(device)->gap_x; +} + +static int32_t st7735_get_gap_y(Device* device) { + return GET_CONFIG(device)->gap_y; +} + +static error_t st7735_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 st7735_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 st7735_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; +} + +static enum DisplayColorFormat st7735_get_color_format(Device* device) { + const auto* config = GET_CONFIG(device); + return config->bgr_order ? DISPLAY_COLOR_FORMAT_BGR565 : DISPLAY_COLOR_FORMAT_RGB565; +} + +static uint16_t st7735_get_resolution_x(Device* device) { + return GET_CONFIG(device)->horizontal_resolution; +} + +static uint16_t st7735_get_resolution_y(Device* device) { + return GET_CONFIG(device)->vertical_resolution; +} + +static void st7735_get_frame_buffer(Device*, uint8_t, void** out_buffer) { + *out_buffer = nullptr; +} + +static uint8_t st7735_get_frame_buffer_count(Device*) { + return 0; +} + +static error_t st7735_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 st7735_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 = st7735_reset, + .init = st7735_init, + .draw_bitmap = st7735_draw_bitmap, + .mirror = st7735_mirror, + .swap_xy = st7735_swap_xy, + .get_swap_xy = st7735_get_swap_xy, + .get_mirror_x = st7735_get_mirror_x, + .get_mirror_y = st7735_get_mirror_y, + .set_gap = st7735_set_gap, + .get_gap_x = st7735_get_gap_x, + .get_gap_y = st7735_get_gap_y, + .invert_color = st7735_invert_color, + .disp_on_off = st7735_disp_on_off, + .disp_sleep = st7735_disp_sleep, + .get_color_format = st7735_get_color_format, + .get_resolution_x = st7735_get_resolution_x, + .get_resolution_y = st7735_get_resolution_y, + .get_frame_buffer = st7735_get_frame_buffer, + .get_frame_buffer_count = st7735_get_frame_buffer_count, + .get_backlight = st7735_get_backlight, + .has_capability = nullptr, +}; + +Driver st7735_driver = { + .name = "st7735", + .compatible = (const char*[]) { "sitronix,st7735", nullptr }, + .start_device = start, + .stop_device = stop, + .api = &st7735_display_api, + .device_type = &DISPLAY_TYPE, + .owner = &st7735_module, + .internal = nullptr +}; diff --git a/Drivers/st7789-module/source/st7789.cpp b/Drivers/st7789-module/source/st7789.cpp index 1f58369ff..06b704f4c 100644 --- a/Drivers/st7789-module/source/st7789.cpp +++ b/Drivers/st7789-module/source/st7789.cpp @@ -138,10 +138,11 @@ static error_t start(Device* device) { 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 (confirmed via + // ESP-IDF's esp_lcd_panel_st7789.c), so gap_x/gap_y are passed through unswapped. if (ok) { - int gap_x = config->swap_xy ? config->gap_y : config->gap_x; - int gap_y = config->swap_xy ? config->gap_x : config->gap_y; - ok = (gap_x == 0 && gap_y == 0) || esp_lcd_panel_set_gap(internal->panel_handle, gap_x, gap_y) == ESP_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); @@ -250,6 +251,15 @@ static error_t st7789_set_gap(Device* device, int32_t x_gap, int32_t y_gap) { return esp_lcd_panel_set_gap(internal->panel_handle, x_gap, y_gap) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE; } +// Reads the devicetree-configured baseline, not live hardware state - see DisplayApi::get_gap_x(). +static int32_t st7789_get_gap_x(Device* device) { + return GET_CONFIG(device)->gap_x; +} + +static int32_t st7789_get_gap_y(Device* device) { + return GET_CONFIG(device)->gap_y; +} + static error_t st7789_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; @@ -265,10 +275,6 @@ static error_t st7789_disp_sleep(Device* device, bool sleep) { return esp_lcd_panel_disp_sleep(internal->panel_handle, sleep) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE; } -// SPI panels have no direct-mapped frame buffer; every draw is an SPI transaction, so the color -// format here is derived purely from the devicetree config (bgr_order), not queried from hardware. -// Only 16bpp (RGB565/BGR565) is representable in DisplayColorFormat; other bit depths are not -// distinguishable and fall back to the 16bpp mapping. static enum DisplayColorFormat st7789_get_color_format(Device* device) { const auto* config = GET_CONFIG(device); return config->bgr_order ? DISPLAY_COLOR_FORMAT_BGR565 : DISPLAY_COLOR_FORMAT_RGB565; @@ -314,6 +320,8 @@ static const DisplayApi st7789_display_api = { .get_mirror_x = st7789_get_mirror_x, .get_mirror_y = st7789_get_mirror_y, .set_gap = st7789_set_gap, + .get_gap_x = st7789_get_gap_x, + .get_gap_y = st7789_get_gap_y, .invert_color = st7789_invert_color, .disp_on_off = st7789_disp_on_off, .disp_sleep = st7789_disp_sleep, diff --git a/Drivers/st7796-module/source/st7796.cpp b/Drivers/st7796-module/source/st7796.cpp index 9ea85c414..669c3b7b5 100644 --- a/Drivers/st7796-module/source/st7796.cpp +++ b/Drivers/st7796-module/source/st7796.cpp @@ -138,10 +138,12 @@ static error_t start(Device* device) { 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 the deprecated HAL's St7796Display, which called + // esp_lcd_panel_set_gap(gapX, gapY) directly). if (ok) { - int gap_x = config->swap_xy ? config->gap_y : config->gap_x; - int gap_y = config->swap_xy ? config->gap_x : config->gap_y; - ok = (gap_x == 0 && gap_y == 0) || esp_lcd_panel_set_gap(internal->panel_handle, gap_x, gap_y) == ESP_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); @@ -250,6 +252,15 @@ static error_t st7796_set_gap(Device* device, int32_t x_gap, int32_t y_gap) { return esp_lcd_panel_set_gap(internal->panel_handle, x_gap, y_gap) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE; } +// Reads the devicetree-configured baseline, not live hardware state - see DisplayApi::get_gap_x(). +static int32_t st7796_get_gap_x(Device* device) { + return GET_CONFIG(device)->gap_x; +} + +static int32_t st7796_get_gap_y(Device* device) { + return GET_CONFIG(device)->gap_y; +} + static error_t st7796_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; @@ -317,6 +328,8 @@ static const DisplayApi st7796_display_api = { .get_mirror_x = st7796_get_mirror_x, .get_mirror_y = st7796_get_mirror_y, .set_gap = st7796_set_gap, + .get_gap_x = st7796_get_gap_x, + .get_gap_y = st7796_get_gap_y, .invert_color = st7796_invert_color, .disp_on_off = st7796_disp_on_off, .disp_sleep = st7796_disp_sleep, diff --git a/Drivers/sy6970-module/CMakeLists.txt b/Drivers/sy6970-module/CMakeLists.txt new file mode 100644 index 000000000..930470f41 --- /dev/null +++ b/Drivers/sy6970-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(sy6970-module + SRCS ${SOURCE_FILES} + INCLUDE_DIRS include/ + REQUIRES TactilityKernel +) diff --git a/Drivers/sy6970-module/bindings/silergy,sy6970.yaml b/Drivers/sy6970-module/bindings/silergy,sy6970.yaml new file mode 100644 index 000000000..04c837edb --- /dev/null +++ b/Drivers/sy6970-module/bindings/silergy,sy6970.yaml @@ -0,0 +1,21 @@ +description: > + Silergy SY6970 Li-ion battery charger. Register-compatible with the BQ25896 for everything + used here. Creates a "power-supply" child device (see drivers/sy6970.h) exposing charging + status, charge enable/disable and ship-mode power-off; battery current/voltage/charge-level + are not available from this chip alone (they come from a separate fuel gauge, if present). + +include: ["i2c-device.yaml"] + +compatible: "silergy,sy6970" + +bus: i2c + +properties: + charge-voltage-mv: + type: int + default: 4288 + description: Target battery charge voltage in mV (3840-4608, 16 mV steps) + charge-current-ma: + type: int + default: 1024 + description: Target fast-charge current in mA (0-8128, 64 mA steps) diff --git a/Drivers/sy6970-module/devicetree.yaml b/Drivers/sy6970-module/devicetree.yaml new file mode 100644 index 000000000..a07d6f334 --- /dev/null +++ b/Drivers/sy6970-module/devicetree.yaml @@ -0,0 +1,3 @@ +dependencies: + - TactilityKernel +bindings: bindings diff --git a/Drivers/sy6970-module/include/bindings/sy6970.h b/Drivers/sy6970-module/include/bindings/sy6970.h new file mode 100644 index 000000000..75e33560a --- /dev/null +++ b/Drivers/sy6970-module/include/bindings/sy6970.h @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include + +DEFINE_DEVICETREE(sy6970, struct Sy6970Config) diff --git a/Drivers/sy6970-module/include/drivers/sy6970.h b/Drivers/sy6970-module/include/drivers/sy6970.h new file mode 100644 index 000000000..a5ec14fef --- /dev/null +++ b/Drivers/sy6970-module/include/drivers/sy6970.h @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +struct Sy6970Config { + // Devicetree address hint (0x6A on the LilyGO T-Deck Max; older BQ25896-based boards use 0x6B). + uint8_t address; + uint32_t charge_voltage_mv; + uint32_t charge_current_ma; +}; + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/sy6970-module/include/sy6970_module.h b/Drivers/sy6970-module/include/sy6970_module.h new file mode 100644 index 000000000..c76d28105 --- /dev/null +++ b/Drivers/sy6970-module/include/sy6970_module.h @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +extern struct Module sy6970_module; + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/sy6970-module/source/module.cpp b/Drivers/sy6970-module/source/module.cpp new file mode 100644 index 000000000..b27fe0e93 --- /dev/null +++ b/Drivers/sy6970-module/source/module.cpp @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include + +extern "C" { + +extern Driver sy6970_driver; +extern Driver sy6970_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(&sy6970_power_supply_driver) == ERROR_NONE); + check(driver_construct_add(&sy6970_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(&sy6970_driver) == ERROR_NONE); + check(driver_remove_destruct(&sy6970_power_supply_driver) == ERROR_NONE); + return ERROR_NONE; +} + +Module sy6970_module = { + .name = "sy6970", + .start = start, + .stop = stop, + .symbols = nullptr, + .internal = nullptr +}; + +} // extern "C" diff --git a/Drivers/sy6970-module/source/sy6970.cpp b/Drivers/sy6970-module/source/sy6970.cpp new file mode 100644 index 000000000..84a935ae1 --- /dev/null +++ b/Drivers/sy6970-module/source/sy6970.cpp @@ -0,0 +1,251 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include + +#include +#include +#include +#include +#include +#include + +#include + +#define TAG "SY6970" +#define GET_CONFIG(device) (static_cast((device)->config)) +// The power-supply child's config pointer isn't set; it reads its settings from its parent +// (the sy6970 device) instead - same convention as TactilityKernel's battery_sense.cpp. +#define GET_PARENT_CONFIG(device) (static_cast(device_get_parent(device)->config)) +#define GET_I2C_BUS(device) (device_get_parent(device_get_parent(device))) + +static const TickType_t I2C_TIMEOUT = pdMS_TO_TICKS(50); + +// Register map is BQ25896-compatible for everything used here (see the vendor factory firmware's +// XPowersLib SY6970 init). +static constexpr uint8_t SY6970_REG_03 = 0x03; // bit4 CHG_CONFIG: charge enable +static constexpr uint8_t SY6970_REG_04 = 0x04; // bits[6:0] ICHG: charge current, 64 mA/LSB +static constexpr uint8_t SY6970_REG_06 = 0x06; // bits[7:2] VREG: charge voltage, 3840 mV + n*16 mV +static constexpr uint8_t SY6970_REG_07 = 0x07; // bits[5:4] WATCHDOG: I2C watchdog timer +static constexpr uint8_t SY6970_REG_09 = 0x09; +static constexpr uint8_t SY6970_REG_0B = 0x0B; // read-only status +static constexpr uint8_t SY6970_CHG_CONFIG = 1 << 4; +static constexpr uint8_t SY6970_BATFET_DIS = 1 << 5; // Force BATFET off = ship mode + +// The I2C watchdog must be disabled first, otherwise the chip reverts custom register values to +// defaults after the watchdog expires. Input current limit (REG00) is left at the hardware +// PSEL/ILIM default. Non-fatal: charging still works on chip defaults if any write fails. +static bool configure_charger(Device* i2c, uint8_t address, uint32_t charge_voltage_mv, uint32_t charge_current_ma) { + if (i2c_controller_register8_reset_bits(i2c, address, SY6970_REG_07, 0x30, I2C_TIMEOUT) != ERROR_NONE) { + LOG_E(TAG, "Failed to disable SY6970 watchdog"); + return false; + } + + uint8_t vreg_n = static_cast((charge_voltage_mv - 3840) / 16); + uint8_t reg06 = 0; + if (i2c_controller_register8_get(i2c, address, SY6970_REG_06, ®06, I2C_TIMEOUT) != ERROR_NONE || + i2c_controller_register8_set(i2c, address, SY6970_REG_06, static_cast((reg06 & 0x03) | (vreg_n << 2)), I2C_TIMEOUT) != ERROR_NONE) { + LOG_E(TAG, "Failed to set SY6970 charge voltage"); + return false; + } + + uint8_t ichg_n = static_cast(charge_current_ma / 64); + uint8_t reg04 = 0; + if (i2c_controller_register8_get(i2c, address, SY6970_REG_04, ®04, I2C_TIMEOUT) != ERROR_NONE || + i2c_controller_register8_set(i2c, address, SY6970_REG_04, static_cast((reg04 & 0x80) | ichg_n), I2C_TIMEOUT) != ERROR_NONE) { + LOG_E(TAG, "Failed to set SY6970 charge current"); + return false; + } + + LOG_I(TAG, "SY6970 configured: %lu mV / %lu mA, watchdog off", (unsigned long)charge_voltage_mv, (unsigned long)charge_current_ma); + return true; +} + +extern "C" { + +// region PowerSupplyApi (power-supply child device) + +static bool supports_property(Device*, PowerSupplyProperty property) { + // Battery current/voltage/charge-level aren't available from the SY6970 alone - they come + // from a separate fuel gauge, if the board has one. + return property == POWER_SUPPLY_PROP_IS_CHARGING; +} + +static error_t get_property(Device* device, PowerSupplyProperty property, PowerSupplyPropertyValue* out_value) { + if (property != POWER_SUPPLY_PROP_IS_CHARGING) { + return ERROR_NOT_SUPPORTED; + } + + auto* i2c = GET_I2C_BUS(device); + const auto* config = GET_PARENT_CONFIG(device); + + // REG0B CHRG_STAT bits[4:3]: 0 = not charging, 1 = pre-charge, 2 = fast charge, 3 = charge + // done. Only 1|2 count as charging - "done" must not. + uint8_t reg0b = 0; + if (i2c_controller_register8_get(i2c, config->address, SY6970_REG_0B, ®0b, I2C_TIMEOUT) != ERROR_NONE) { + return ERROR_NOT_FOUND; + } + uint8_t charge_status = (reg0b >> 3) & 0x03; + out_value->int_value = (charge_status == 1 || charge_status == 2) ? 1 : 0; + return ERROR_NONE; +} + +static bool supports_charge_control(Device*) { + return true; +} + +static bool is_allowed_to_charge(Device* device) { + auto* i2c = GET_I2C_BUS(device); + const auto* config = GET_PARENT_CONFIG(device); + uint8_t reg03 = 0; + if (i2c_controller_register8_get(i2c, config->address, SY6970_REG_03, ®03, I2C_TIMEOUT) != ERROR_NONE) { + return false; + } + return (reg03 & SY6970_CHG_CONFIG) != 0; +} + +static error_t set_allowed_to_charge(Device* device, bool allowed) { + auto* i2c = GET_I2C_BUS(device); + const auto* config = GET_PARENT_CONFIG(device); + return allowed + ? i2c_controller_register8_set_bits(i2c, config->address, SY6970_REG_03, SY6970_CHG_CONFIG, I2C_TIMEOUT) + : i2c_controller_register8_reset_bits(i2c, config->address, SY6970_REG_03, SY6970_CHG_CONFIG, I2C_TIMEOUT); +} + +// Quick charge (HVDCP/PD boost) is not implemented: the ported register set doesn't cover it. +static bool supports_quick_charge(Device*) { return false; } +static bool is_quick_charge_enabled(Device*) { return false; } +static error_t set_quick_charge_enabled(Device*, bool) { return ERROR_NOT_SUPPORTED; } + +static bool supports_power_off(Device*) { + return true; +} + +static error_t power_off(Device* device) { + // Ship mode: force the charger's BATFET off (REG09 bit5). Mirrors the vendor XPowersLib + // PowersSY6970::shutdown(). Note: this only fully powers the board down when running on + // battery with USB unplugged. + auto* i2c = GET_I2C_BUS(device); + const auto* config = GET_PARENT_CONFIG(device); + LOG_I(TAG, "Power off (SY6970 BATFET_DIS)"); + return i2c_controller_register8_set_bits(i2c, config->address, SY6970_REG_09, SY6970_BATFET_DIS, I2C_TIMEOUT); +} + +static constexpr PowerSupplyApi SY6970_POWER_SUPPLY_API = { + .supports_property = supports_property, + .get_property = get_property, + .supports_charge_control = supports_charge_control, + .is_allowed_to_charge = is_allowed_to_charge, + .set_allowed_to_charge = set_allowed_to_charge, + .supports_quick_charge = supports_quick_charge, + .is_quick_charge_enabled = is_quick_charge_enabled, + .set_quick_charge_enabled = set_quick_charge_enabled, + .supports_power_off = supports_power_off, + .power_off = power_off, +}; + +// Registered (driver_construct_add() in module.cpp) so driver_bind() has a valid ->internal, but +// never matched against a devicetree node: sy6970_driver wires it up directly by pointer. +Driver sy6970_power_supply_driver = { + .name = "sy6970-power-supply", + .compatible = (const char*[]) { "sy6970-power-supply", nullptr }, + .start_device = nullptr, + .stop_device = nullptr, + .api = &SY6970_POWER_SUPPLY_API, + .device_type = &POWER_SUPPLY_TYPE, + .owner = &sy6970_module, + .internal = nullptr +}; + +// endregion + +// region Driver lifecycle (sy6970 device itself) + +struct Sy6970Internal { + 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 = "sy6970-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, &sy6970_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; +} + +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); + configure_charger(parent, config->address, config->charge_voltage_mv, config->charge_current_ma); + + auto* internal = new (std::nothrow) Sy6970Internal(); + 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); + 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; +} + +Driver sy6970_driver = { + .name = "sy6970", + .compatible = (const char*[]) { "silergy,sy6970", nullptr }, + .start_device = start, + .stop_device = stop, + .api = nullptr, + .device_type = nullptr, + .owner = &sy6970_module, + .internal = nullptr +}; + +// endregion + +} diff --git a/Drivers/tca8418-module/CMakeLists.txt b/Drivers/tca8418-module/CMakeLists.txt new file mode 100644 index 000000000..5f5167051 --- /dev/null +++ b/Drivers/tca8418-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(tca8418-module + SRCS ${SOURCE_FILES} + INCLUDE_DIRS include/ + REQUIRES TactilityKernel +) diff --git a/Drivers/tca8418-module/bindings/ti,tca8418.yaml b/Drivers/tca8418-module/bindings/ti,tca8418.yaml new file mode 100644 index 000000000..292a62c9e --- /dev/null +++ b/Drivers/tca8418-module/bindings/ti,tca8418.yaml @@ -0,0 +1,74 @@ +description: > + Texas Instruments TCA8418 I2C keypad matrix scanner. Exposed as a generic KEYBOARD_TYPE + device: the driver only scans the matrix and reports raw key events, while the character + layout comes entirely from the keymap-lc/keymap-uc/keymap-sy devicetree properties, so the + same driver works for any board's physical key layout. + +include: ["i2c-device.yaml"] + +compatible: "ti,tca8418" + +bus: i2c + +properties: + rows: + type: int + required: true + description: Number of keypad matrix rows wired (max 8) + columns: + type: int + required: true + description: Number of keypad matrix columns wired (max 10) + reverse-columns: + type: boolean + default: false + description: > + Some boards wire columns in reverse of their silkscreen/keymap order; when true, the raw + hardware column is mirrored (columns-1-col) before indexing into the keymap arrays and + before comparing against shift-row/shift-col/sym-row/sym-col. + keymap-lc: + type: array + element-type: uint8_t + required: true + description: > + Base (lowercase) layer keymap, rows*columns bytes in row-major order (already in + silkscreen/keymap column order - see reverse-columns). 0 = no key at this position (e.g. + a blank matrix position, or a position handled as a modifier via shift-row/shift-col/ + sym-row/sym-col instead). Non-zero bytes are sent as-is via KeyboardKeyData::key (ASCII + or an LVGL LV_KEY_* code). + keymap-uc: + type: array + element-type: uint8_t + description: > + Uppercase/shift layer keymap, same shape as keymap-lc. Omit if this keyboard has no + separate uppercase layer (shift-row/shift-col should then also be left unset). + keymap-sy: + type: array + element-type: uint8_t + description: > + Symbol layer keymap, same shape as keymap-lc. Omit if this keyboard has no symbol layer + (sym-row/sym-col should then also be left unset). + shift-row: + type: int + default: 255 + description: Matrix row (keymap column order) of the shift/uppercase modifier key, 255 if none + shift-col: + type: int + default: 255 + description: Matrix column (keymap column order) of the shift/uppercase modifier key, 255 if none + sym-row: + type: int + default: 255 + description: Matrix row (keymap column order) of the symbol-layer modifier key, 255 if none + sym-col: + type: int + default: 255 + description: Matrix column (keymap column order) of the symbol-layer modifier key, 255 if none + pin-reset: + type: phandles + default: GPIO_PIN_SPEC_NONE + description: Optional reset GPIO pin, pulsed once at start (20 ms assert, 60 ms settle) + reset-active-high: + type: boolean + default: false + description: Whether the reset pin is active high diff --git a/Drivers/tca8418-module/devicetree.yaml b/Drivers/tca8418-module/devicetree.yaml new file mode 100644 index 000000000..a07d6f334 --- /dev/null +++ b/Drivers/tca8418-module/devicetree.yaml @@ -0,0 +1,3 @@ +dependencies: + - TactilityKernel +bindings: bindings diff --git a/Drivers/tca8418-module/include/bindings/tca8418.h b/Drivers/tca8418-module/include/bindings/tca8418.h new file mode 100644 index 000000000..b75e75e16 --- /dev/null +++ b/Drivers/tca8418-module/include/bindings/tca8418.h @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include + +DEFINE_DEVICETREE(tca8418, struct Tca8418Config) diff --git a/Drivers/tca8418-module/include/drivers/tca8418.h b/Drivers/tca8418-module/include/drivers/tca8418.h new file mode 100644 index 000000000..22b52a321 --- /dev/null +++ b/Drivers/tca8418-module/include/drivers/tca8418.h @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include + +struct Tca8418Config { + // Devicetree address hint (0x34 on the LilyGO T-Deck Max). + uint8_t address; + uint8_t rows; + uint8_t columns; + bool reverse_columns; + const uint8_t* keymap_lc; + uint32_t keymap_lc_length; + const uint8_t* keymap_uc; + uint32_t keymap_uc_length; + const uint8_t* keymap_sy; + uint32_t keymap_sy_length; + uint8_t shift_row; + uint8_t shift_col; + uint8_t sym_row; + uint8_t sym_col; + struct GpioPinSpec pin_reset; + bool reset_active_high; +}; + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/tca8418-module/include/tca8418_module.h b/Drivers/tca8418-module/include/tca8418_module.h new file mode 100644 index 000000000..e98386ca5 --- /dev/null +++ b/Drivers/tca8418-module/include/tca8418_module.h @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +extern struct Module tca8418_module; + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/tca8418-module/source/module.cpp b/Drivers/tca8418-module/source/module.cpp new file mode 100644 index 000000000..e5ab6e7e1 --- /dev/null +++ b/Drivers/tca8418-module/source/module.cpp @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include + +extern "C" { + +extern Driver tca8418_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(&tca8418_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(&tca8418_driver) == ERROR_NONE); + return ERROR_NONE; +} + +Module tca8418_module = { + .name = "tca8418", + .start = start, + .stop = stop, + .symbols = nullptr, + .internal = nullptr +}; + +} // extern "C" diff --git a/Drivers/tca8418-module/source/tca8418.cpp b/Drivers/tca8418-module/source/tca8418.cpp new file mode 100644 index 000000000..fa974f2f4 --- /dev/null +++ b/Drivers/tca8418-module/source/tca8418.cpp @@ -0,0 +1,284 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#define TAG "TCA8418" +#define GET_CONFIG(device) (static_cast((device)->config)) + +static const TickType_t I2C_TIMEOUT = pdMS_TO_TICKS(50); + +// TCA8418 register map (https://www.ti.com/lit/ds/symlink/tca8418.pdf). +static constexpr uint8_t REG_CFG = 0x01; +static constexpr uint8_t REG_KEY_EVENT_A = 0x04; +static constexpr uint8_t REG_KP_GPIO1 = 0x1D; +static constexpr uint8_t REG_KP_GPIO2 = 0x1E; +static constexpr uint8_t REG_KP_GPIO3 = 0x1F; + +// The KEY_EVENT register always encodes a key's position as row*10+col+1, using a fixed +// 10-column addressing space regardless of how many columns are actually wired/enabled (a +// TCA8418 hardware constant, not related to Tca8418Config::columns). +static constexpr uint8_t KEY_EVENT_ENCODING_WIDTH = 10; + +static constexpr uint8_t NO_MODIFIER = 255; + +// Worst case per read_key() poll: 16 drained hardware events, each a char key producing a +// press+release pair. +static constexpr uint8_t PENDING_CAPACITY = 32; + +struct Tca8418PendingEvent { + uint32_t key; + bool pressed; +}; + +struct Tca8418Internal { + bool shift_pressed; + bool sym_pressed; + bool cap_toggle; + bool cap_toggle_armed; + Tca8418PendingEvent pending[PENDING_CAPACITY]; + uint8_t pending_head; + uint8_t pending_count; +}; + +static void push_pending(Tca8418Internal* internal, uint32_t key, bool pressed) { + if (internal->pending_count >= PENDING_CAPACITY) { + LOG_W(TAG, "Pending event queue full, dropping event"); + return; + } + uint8_t tail = (internal->pending_head + internal->pending_count) % PENDING_CAPACITY; + internal->pending[tail] = { .key = key, .pressed = pressed }; + internal->pending_count++; +} + +static bool pop_pending(Tca8418Internal* internal, Tca8418PendingEvent* out_event) { + if (internal->pending_count == 0) { + return false; + } + *out_event = internal->pending[internal->pending_head]; + internal->pending_head = (internal->pending_head + 1) % PENDING_CAPACITY; + internal->pending_count--; + return true; +} + +// region Driver lifecycle + +// Ported from Adafruit_TCA8418::matrix() (see Drivers/TCA8418's now-removed deprecated-HAL +// equivalent / https://github.com/adafruit/Adafruit_TCA8418). +static bool init_matrix(Device* i2c, uint8_t address, uint8_t rows, uint8_t columns) { + if (rows > 8 || columns > 10) { + return false; + } + if (rows == 0 || columns == 0) { + return true; + } + + uint8_t mask = 0; + for (int r = 0; r < rows; r++) { + mask = static_cast((mask << 1) | 1); + } + if (i2c_controller_register8_set(i2c, address, REG_KP_GPIO1, mask, I2C_TIMEOUT) != ERROR_NONE) { + return false; + } + + mask = 0; + for (int c = 0; c < columns && c < 8; c++) { + mask = static_cast((mask << 1) | 1); + } + if (i2c_controller_register8_set(i2c, address, REG_KP_GPIO2, mask, I2C_TIMEOUT) != ERROR_NONE) { + return false; + } + + if (columns > 8) { + mask = (columns == 9) ? 0x01 : 0x03; + if (i2c_controller_register8_set(i2c, address, REG_KP_GPIO3, mask, I2C_TIMEOUT) != ERROR_NONE) { + return false; + } + } + return true; +} + +// Some boards route this chip's reset line through an external GPIO/IO-expander pin that must +// be released before the chip will respond (see Tca8418Config::pin_reset). +static void perform_hardware_reset(const Tca8418Config* config) { + if (config->pin_reset.gpio_controller == nullptr) { + return; + } + + auto* rst = gpio_descriptor_acquire(config->pin_reset.gpio_controller, config->pin_reset.pin, GPIO_OWNER_GPIO); + if (rst == nullptr) { + return; + } + + gpio_descriptor_set_flags(rst, GPIO_FLAG_DIRECTION_OUTPUT); + gpio_descriptor_set_level(rst, config->reset_active_high); + delay_millis(20); + gpio_descriptor_set_level(rst, !config->reset_active_high); + gpio_descriptor_release(rst); + delay_millis(60); +} + +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(calloc(1, sizeof(Tca8418Internal))); + if (internal == nullptr) { + return ERROR_OUT_OF_MEMORY; + } + internal->cap_toggle_armed = true; + + perform_hardware_reset(config); + + if (!init_matrix(parent, config->address, config->rows, config->columns)) { + LOG_E(TAG, "Failed to configure keypad matrix"); + free(internal); + return ERROR_RESOURCE; + } + + // BIT7 AI=1 auto-increment, BIT6 GPI_E_CFG=0, BIT5 OVR_FLOW_M=0 (overflow data lost), BIT4 + // INT_CFG=1, others 0: 0x99. Matches the vendor factory firmware / Adafruit_TCA8418 default. + if (i2c_controller_register8_set(parent, config->address, REG_CFG, 0x99, I2C_TIMEOUT) != ERROR_NONE) { + LOG_E(TAG, "Failed to configure keypad controller"); + 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)); + free(internal); + device_set_driver_data(device, nullptr); + return ERROR_NONE; +} + +// endregion + +// region KeyboardApi + +static uint8_t keymap_lookup(const uint8_t* keymap, uint32_t keymap_length, uint8_t row, uint8_t vcol, uint8_t columns) { + uint32_t index = static_cast(row) * columns + vcol; + if (keymap == nullptr || index >= keymap_length) { + return 0; + } + return keymap[index]; +} + +static void handle_key_event(Tca8418Internal* internal, const Tca8418Config* config, uint8_t event) { + bool key_down = (event & 0x80) != 0; + uint8_t code = static_cast((event & 0x7F) - 1); + uint8_t row = static_cast(code / KEY_EVENT_ENCODING_WIDTH); + uint8_t col = static_cast(code % KEY_EVENT_ENCODING_WIDTH); + uint8_t vcol = config->reverse_columns ? static_cast(config->columns - 1 - col) : col; + + bool is_shift = row == config->shift_row && vcol == config->shift_col; + bool is_sym = row == config->sym_row && vcol == config->sym_col; + + if (!key_down) { + // Release event: only modifier state cares about releases. + if (is_shift) { + internal->shift_pressed = false; + } + if (is_sym) { + internal->sym_pressed = false; + } + } else { + if (is_shift) { + internal->shift_pressed = true; + } else if (is_sym) { + internal->sym_pressed = true; + } else { + const uint8_t* keymap = config->keymap_lc; + uint32_t keymap_length = config->keymap_lc_length; + if (internal->sym_pressed) { + keymap = config->keymap_sy; + keymap_length = config->keymap_sy_length; + } else if (internal->shift_pressed || internal->cap_toggle) { + keymap = config->keymap_uc; + keymap_length = config->keymap_uc_length; + } + uint8_t chr = keymap_lookup(keymap, keymap_length, row, vcol, config->columns); + if (chr != 0) { + // LVGL only registers a key on a RELEASED->PRESSED edge, so every keystroke is + // queued as an immediate press+release pair. + push_pending(internal, chr, true); + push_pending(internal, chr, false); + } + } + + if (internal->sym_pressed && internal->shift_pressed && internal->cap_toggle_armed && + config->sym_row != NO_MODIFIER && config->shift_row != NO_MODIFIER) { + internal->cap_toggle = !internal->cap_toggle; + internal->cap_toggle_armed = false; + } + } + + if (!internal->sym_pressed && !internal->shift_pressed) { + internal->cap_toggle_armed = true; + } +} + +static error_t tca8418_read_key(Device* device, KeyboardKeyData* data) { + auto* internal = static_cast(device_get_driver_data(device)); + const auto* config = GET_CONFIG(device); + auto* parent = device_get_parent(device); + + // Each register read pops one event from the TCA8418's FIFO. Drain it fully per call + // (bounded, in case of a misbehaving bus) so a fast typing burst isn't throttled to one + // event per LVGL indev poll period. + for (int drained = 0; drained < 16; drained++) { + uint8_t event = 0; + if (i2c_controller_register8_get(parent, config->address, REG_KEY_EVENT_A, &event, I2C_TIMEOUT) != ERROR_NONE) { + break; + } + if (event == 0) { + break; + } + handle_key_event(internal, config, event); + } + + Tca8418PendingEvent event; + if (pop_pending(internal, &event)) { + data->key = event.key; + data->pressed = event.pressed; + data->continue_reading = internal->pending_count > 0; + } else { + data->key = 0; + data->pressed = false; + data->continue_reading = false; + } + return ERROR_NONE; +} + +// endregion + +static constexpr KeyboardApi tca8418_api = { + .read_key = tca8418_read_key, +}; + +Driver tca8418_driver = { + .name = "tca8418", + .compatible = (const char*[]) { "ti,tca8418", nullptr }, + .start_device = start, + .stop_device = stop, + .api = &tca8418_api, + .device_type = &KEYBOARD_TYPE, + .owner = &tca8418_module, + .internal = nullptr +}; diff --git a/Modules/lvgl-module/include/tactility/lvgl_display.h b/Modules/lvgl-module/include/tactility/lvgl_display.h index eee2f8c7b..c3b396a02 100644 --- a/Modules/lvgl-module/include/tactility/lvgl_display.h +++ b/Modules/lvgl-module/include/tactility/lvgl_display.h @@ -35,6 +35,15 @@ struct LvglDisplayConfig { * Allocates one extra buffer sized like the primary draw buffer. */ bool sw_rotate; + + /** + * Endianness of the 2 bytes of each RGB565/BGR565 pixel sent to the panel. False (default) + * keeps this little-endian CPU's native byte order (no-op). True swaps the 2 bytes of every + * pixel (big-endian) in the flush callback, via lv_draw_sw_rgb565_swap() - for panels that + * expect the opposite byte order over the bus. Ignored for color formats other than + * RGB565/BGR565 (e.g. RGB888, MONOCHROME). + */ + bool swap_bytes; }; /** diff --git a/Modules/lvgl-module/source/lvgl_devices.c b/Modules/lvgl-module/source/lvgl_devices.c index 1c74846b0..10a5cc170 100644 --- a/Modules/lvgl-module/source/lvgl_devices.c +++ b/Modules/lvgl-module/source/lvgl_devices.c @@ -37,8 +37,15 @@ void lvgl_devices_attach() { // sized to 1/10th of the vertical resolution; it fits comfortably everywhere, // including boards with PSRAM that could afford full-frame. uint16_t vres = display_get_resolution_y(kernel_display_device); + // _SWAPPED variants need it outright; plain BGR565 panels have also been found on real + // hardware to need it alongside their MADCTL BGR bit (see st7735-module/st7789-module). + enum DisplayColorFormat color_format = display_get_color_format(kernel_display_device); + bool swap_bytes = color_format == DISPLAY_COLOR_FORMAT_RGB565_SWAPPED || + color_format == DISPLAY_COLOR_FORMAT_BGR565_SWAPPED || + color_format == DISPLAY_COLOR_FORMAT_BGR565; struct LvglDisplayConfig lvgl_display_config = { - .buffer_height = vres > 10 ? vres / 10 : vres + .buffer_height = vres > 10 ? vres / 10 : vres, + .swap_bytes = swap_bytes }; if (lvgl_display_add(kernel_display_device, &lvgl_display_config, &lvgl_display) == ERROR_NONE) { LOG_I(TAG, "Bound %s to LVGL", kernel_display_device->name); diff --git a/Modules/lvgl-module/source/lvgl_display.c b/Modules/lvgl-module/source/lvgl_display.c index dcc91749c..3447b3361 100644 --- a/Modules/lvgl-module/source/lvgl_display.c +++ b/Modules/lvgl-module/source/lvgl_display.c @@ -1,6 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 #include +#include +#include #include #include @@ -17,10 +19,20 @@ struct LvglDisplayCtx { void* buf1; void* buf2; bool owns_buffers; // false when buf1/buf2 point at the device's own frame buffer(s) + // Mirrors what lvgl_display_add() passed to lv_display_set_render_mode() - there's no + // lv_display_get_render_mode() to query it back from LVGL, so it's cached here instead. + lv_display_render_mode_t render_mode; // The device's swap_xy/mirror_x/mirror_y at bind time, queried once and treated as the LV_DISPLAY_ROTATION_0 baseline. bool base_swap_xy; bool base_mirror_x; bool base_mirror_y; + // The device's configured gap at bind time, in the same LV_DISPLAY_ROTATION_0 baseline frame + // as base_swap_xy above. set_gap() is a raw (x,y) offset applied to whatever coordinates the + // panel is currently being drawn with - it has no idea about swap_xy, so a rotation that flips + // swap_xy relative to this baseline must swap gap_x/gap_y too (see lvgl_display_apply_rotation()). + int32_t base_gap_x; + int32_t base_gap_y; + bool has_set_gap_cap; // When true, rotation is done in software in the flush callback instead of via display_swap_xy()/ // display_mirror(); rotate_buf holds the rotated pixels and is sized like buf1. bool sw_rotate; @@ -32,12 +44,8 @@ struct LvglDisplayCtx { // null on drivers that don't support them, so rotation handling must not call through blindly. bool has_swap_xy_cap; bool has_mirror_cap; - // True for DISPLAY_COLOR_FORMAT_RGB565_SWAPPED: the panel wants each 16-bit pixel high-byte-first - // over SPI, which this little-endian CPU's native RGB565 buffer isn't. LVGL's own - // LV_COLOR_FORMAT_RGB565_SWAPPED display format looked like the fit but isn't fully supported by - // the SW renderer (produced garbage on real hardware) - esp_lvgl_port's own approach is instead to - // render normal RGB565 and byte-swap the flushed rect in place just before sending it out, via - // lv_draw_sw_rgb565_swap(). Mirrored here. + // Mirrors LvglDisplayConfig::swap_bytes: the panel is big endian while the OS is little endian, + // so we fix it in software. In the future, the driver should probably expose endianness requirements instead. bool byte_swap; }; @@ -61,25 +69,28 @@ static void lvgl_display_free_buffer(void* buf) { #endif } -// out_byte_swap is set whenever the LVGL-side buffer format doesn't already match the panel's -// wire byte order and needs a post-render swap in the flush callback (see LvglDisplayCtx::byte_swap). -static bool lvgl_display_map_color_format(enum DisplayColorFormat in, lv_color_format_t* out, bool* out_byte_swap) { +// Resolves the kernel-reported color format to an LVGL color format. RGB565 and BGR565 (and +// their _SWAPPED variants) all render identically as far as LVGL is concerned - it has no native +// concept of channel order, only byte order (see LvglDisplayConfig::swap_bytes for that axis). +static bool lvgl_display_map_color_format(enum DisplayColorFormat in, lv_color_format_t* out) { switch (in) { case DISPLAY_COLOR_FORMAT_RGB565: - *out = LV_COLOR_FORMAT_RGB565; - *out_byte_swap = false; - return true; case DISPLAY_COLOR_FORMAT_RGB565_SWAPPED: + case DISPLAY_COLOR_FORMAT_BGR565: + case DISPLAY_COLOR_FORMAT_BGR565_SWAPPED: *out = LV_COLOR_FORMAT_RGB565; - *out_byte_swap = true; return true; case DISPLAY_COLOR_FORMAT_RGB888: *out = LV_COLOR_FORMAT_RGB888; - *out_byte_swap = false; + return true; + case DISPLAY_COLOR_FORMAT_MONOCHROME: + // Row-major, MSB-first 1bpp (matches LV_COLOR_FORMAT_I1's raw layout once the + // palette header is stripped, see lvgl_display_flush_cb()) - any page/tile + // reformatting a specific panel's GDDRAM needs is that driver's own concern + // (e.g. ssd1306_draw_bitmap()'s row-to-page transpose). + *out = LV_COLOR_FORMAT_I1; return true; default: - // DISPLAY_COLOR_FORMAT_BGR565/_SWAPPED: no LVGL equivalent (channel order, not byte order). - // DISPLAY_COLOR_FORMAT_MONOCHROME: unsupported for now, no 1bpp conversion buffer implemented. return false; } } @@ -125,6 +136,14 @@ static void lvgl_display_apply_rotation(struct LvglDisplayCtx* ctx, lv_display_r if (ctx->has_mirror_cap) { display_mirror(ctx->device, mirror_x, mirror_y); } + if (ctx->has_set_gap_cap) { + // set_gap() takes its (x,y) in whatever axes the panel is currently drawn with, not the + // baseline's - swap gap_x/gap_y whenever this rotation's swap_xy differs from the baseline. + bool gap_axes_swapped = swap_xy != ctx->base_swap_xy; + int32_t gap_x = gap_axes_swapped ? ctx->base_gap_y : ctx->base_gap_x; + int32_t gap_y = gap_axes_swapped ? ctx->base_gap_x : ctx->base_gap_y; + display_set_gap(ctx->device, gap_x, gap_y); + } } static void lvgl_display_rotation_event_cb(lv_event_t* e) { @@ -143,8 +162,9 @@ static void* lvgl_display_fb_base(struct LvglDisplayCtx* ctx, const uint8_t* col return ctx->buf1; } -static void lvgl_display_flush_cb(lv_display_t* disp, const lv_area_t* area, uint8_t* color_map) { + static void lvgl_display_flush_cb(lv_display_t* disp, const lv_area_t* area, uint8_t* color_map) { struct LvglDisplayCtx* ctx = (struct LvglDisplayCtx*)lv_display_get_driver_data(disp); + bool is_i1 = lv_display_get_color_format(disp) == LV_COLOR_FORMAT_I1; int32_t x1 = area->x1; int32_t y1 = area->y1; @@ -180,21 +200,40 @@ static void lvgl_display_flush_cb(lv_display_t* disp, const lv_area_t* area, uin lv_draw_sw_rgb565_swap(color_map, area_size_px); } - if (ctx->owns_buffers) { + if (ctx->render_mode == LV_DISPLAY_RENDER_MODE_FULL) { + // FULL mode always redraws (and flushes) the whole display, but a refresh cycle can still + // call this flush_cb once per still-unjoined invalidated area (lv_refr.c's + // refr_invalid_areas()), each writing its own tile into the *same* shared buffer - only + // the last call actually holds the complete frame. This applies equally whether that + // buffer is one we own (owns_buffers, e.g. an I1 e-paper/OLED panel) or a real hardware + // frame buffer (fb-direct, where display_draw_bitmap() - see rgb_display_draw_bitmap() - + // additionally blocks for a full scan-out period whenever frame_buffer_count > 0). + // Presenting on every call would send partially-rendered frames, and for fb-direct would + // also pay that scan-out wait N times per refresh instead of once; defer to the last + // flush and present the whole buffer in one call, mirroring esp_lvgl_port_disp.c's own + // lv_disp_flush_is_last() gate for its direct/full render mode. + if (lv_display_flush_is_last(disp)) { + uint8_t* fb_base; + if (ctx->owns_buffers) { + fb_base = (uint8_t*)ctx->buf1; + if (is_i1) { + // LVGL reserves an 8-byte palette (2 x lv_color32_t) at the front of every I1 + // draw buffer; it's on the caller to skip it before treating the rest as + // pixel data. + fb_base += 8; + } + } else { + fb_base = (uint8_t*)lvgl_display_fb_base(ctx, color_map); + } + uint16_t hres = display_get_resolution_x(ctx->device); + uint16_t vres = display_get_resolution_y(ctx->device); + display_draw_bitmap(ctx->device, 0, 0, hres, vres, fb_base); + } + } else if (ctx->owns_buffers) { + // PARTIAL mode: each flush_cb call is one independent, complete tile into a buffer that + // gets reused for the next tile, so present it immediately rather than waiting. // LVGL's area is inclusive; DisplayApi's draw_bitmap wants an exclusive end. display_draw_bitmap(ctx->device, x1, y1, x2 + 1, y2 + 1, color_map); - } else if (lv_display_flush_is_last(disp)) { - // fb-direct: a refresh cycle can call this flush_cb once per still-unjoined invalidated area - // (lv_refr.c's refr_invalid_areas()), each writing its own tile into the *same* shared frame - // buffer - but display_draw_bitmap() (see rgb_display_draw_bitmap()) blocks for a full - // scan-out period whenever frame_buffer_count > 0. Presenting after every tile would pay - // that wait N times per refresh instead of once; defer to the last flush of the cycle and - // present the whole buffer in one call, mirroring esp_lvgl_port_disp.c's - // lv_disp_flush_is_last() gate for its own direct/full render mode. - uint8_t* fb_base = (uint8_t*)lvgl_display_fb_base(ctx, color_map); - uint16_t hres = display_get_resolution_x(ctx->device); - uint16_t vres = display_get_resolution_y(ctx->device); - display_draw_bitmap(ctx->device, 0, 0, hres, vres, fb_base); } // DisplayApi has no async completion callback, so draw_bitmap is synchronous. lv_display_flush_ready(disp); @@ -209,9 +248,8 @@ error_t lvgl_display_add(struct Device* device, const struct LvglDisplayConfig* } lv_color_format_t lv_color_format; - bool byte_swap; enum DisplayColorFormat kernel_color_format = display_get_color_format(device); - if (!lvgl_display_map_color_format(kernel_color_format, &lv_color_format, &byte_swap)) { + if (!lvgl_display_map_color_format(kernel_color_format, &lv_color_format)) { LOG_E(TAG, "Unsupported color format %d (no LVGL equivalent)", (int)kernel_color_format); return ERROR_NOT_SUPPORTED; } @@ -226,31 +264,53 @@ error_t lvgl_display_add(struct Device* device, const struct LvglDisplayConfig* return ERROR_OUT_OF_MEMORY; } ctx->device = device; - ctx->byte_swap = byte_swap; + ctx->byte_swap = config->swap_bytes; ctx->sw_rotate = config->sw_rotate; ctx->has_swap_xy_cap = display_has_capability(device, DISPLAY_CAPABILITY_CAP_SWAP_XY); ctx->has_mirror_cap = display_has_capability(device, DISPLAY_CAPABILITY_CAP_MIRROR); - // fb_count > 0 with capability false means a driver reports mirror/swap_xy unavailable - // specifically because binding fb-direct would defeat them (see rgb_display_has_capability()). - // We're about to fall back to an owned buffer instead (right below) precisely because of that, - // at which point the concern doesn't apply anymore: frame_buffer_count > 0 mattering to - // has_capability() at all implies the driver's mirror()/swap_xy() are real, non-null - // implementations, and the copy-into-fb path honors them correctly once not fb-direct-bound. - // sw_rotate is excluded too: it writes rotated pixels into ctx->rotate_buf, which - // lvgl_display_fb_base() doesn't recognize, so fb-direct must stay off in that case as well. + ctx->has_set_gap_cap = display_has_capability(device, DISPLAY_CAPABILITY_CAP_SET_GAP); + // sw_rotate is excluded from fb-direct binding below: it writes rotated pixels into + // ctx->rotate_buf, which lvgl_display_fb_base() doesn't recognize, so fb-direct must stay + // off in that case as well. bool would_bind_fb_direct = fb_count > 0 && ctx->has_swap_xy_cap && ctx->has_mirror_cap && !ctx->sw_rotate; if (fb_count > 0 && !would_bind_fb_direct) { - ctx->has_swap_xy_cap = true; - ctx->has_mirror_cap = true; + // Only re-enable capabilities the driver reported off above if it has a *dynamic* + // has_capability() (DisplayApi.has_capability non-null, e.g. rgb_display_has_capability()/ + // st7701_has_capability()): that's specifically what a driver implements when a capability's + // availability is state-dependent - here, off only because binding fb-direct would defeat + // it (see rgb_display_has_capability()) - so once we're falling back to an owned buffer + // instead (right below), the concern no longer applies. A driver with no dynamic + // has_capability() reports fixed, hardware-level capabilities instead: if the bit's off, + // swap_xy()/mirror() don't exist at all (null function pointers - see the DisplayApi + // contract), so forcing them back on here would call through a null pointer. + const struct DisplayApi* api = (const struct DisplayApi*)device_get_driver(device)->api; + if (api->has_capability != NULL) { + ctx->has_swap_xy_cap = true; + ctx->has_mirror_cap = true; + } } ctx->base_swap_xy = ctx->has_swap_xy_cap ? display_get_swap_xy(device) : false; ctx->base_mirror_x = ctx->has_mirror_cap ? display_get_mirror_x(device) : false; ctx->base_mirror_y = ctx->has_mirror_cap ? display_get_mirror_y(device) : false; + ctx->base_gap_x = ctx->has_set_gap_cap ? display_get_gap_x(device) : 0; + ctx->base_gap_y = ctx->has_set_gap_cap ? display_get_gap_y(device) : 0; lv_display_render_mode_t render_mode; size_t buf_size_bytes; - if (would_bind_fb_direct) { + if (lv_color_format == LV_COLOR_FORMAT_I1) { + // I1 packs 8 pixels/byte row-wise and LVGL reserves an 8-byte palette header at the + // buffer's start (see lvgl_display_flush_cb()). Always redraw the whole frame in one + // owned buffer instead of computing partial-region byte offsets against that packing. + buf_size_bytes = (size_t)((hres + 7) / 8) * vres + 8; + ctx->buf1 = lvgl_display_alloc_buffer(buf_size_bytes); + if (ctx->buf1 == NULL) { + free(ctx); + return ERROR_OUT_OF_MEMORY; + } + ctx->owns_buffers = true; + render_mode = LV_DISPLAY_RENDER_MODE_FULL; + } else if (would_bind_fb_direct) { display_get_frame_buffer(device, 0, &ctx->buf1); if (fb_count > 1) { display_get_frame_buffer(device, 1, &ctx->buf2); @@ -304,6 +364,7 @@ error_t lvgl_display_add(struct Device* device, const struct LvglDisplayConfig* return ERROR_OUT_OF_MEMORY; } + ctx->render_mode = render_mode; lv_display_set_color_format(disp, lv_color_format); lv_display_set_buffers(disp, ctx->buf1, ctx->buf2, buf_size_bytes, render_mode); lv_display_set_flush_cb(disp, lvgl_display_flush_cb); diff --git a/Modules/lvgl-module/source/lvgl_pointer.c b/Modules/lvgl-module/source/lvgl_pointer.c index f01cf231f..29dbb8f65 100644 --- a/Modules/lvgl-module/source/lvgl_pointer.c +++ b/Modules/lvgl-module/source/lvgl_pointer.c @@ -51,7 +51,6 @@ static void lvgl_pointer_calibration_apply( if (mapped_y < 0) mapped_y = 0; if (mapped_y > target_y_max) mapped_y = target_y_max; - LOG_I(TAG, "Calibration mapping: %d,%d -> %d,%d", *x, *y, (int)mapped_x, (int)mapped_y); *x = (uint16_t)mapped_x; *y = (uint16_t)mapped_y; } diff --git a/Tactility/Source/app/poweroff/PowerOff.cpp b/Tactility/Source/app/poweroff/PowerOff.cpp index a1b6e7e82..1fbba45b5 100644 --- a/Tactility/Source/app/poweroff/PowerOff.cpp +++ b/Tactility/Source/app/poweroff/PowerOff.cpp @@ -29,7 +29,7 @@ class PowerOffApp final : public App { lv_obj_set_flex_align(screen, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); auto* title = lv_label_create(screen); - lv_label_set_text(title, "Tactility OS"); + lv_label_set_text(title, "Tactility"); lv_obj_set_style_text_font(title, lvgl_get_text_font(FONT_SIZE_LARGE), 0); lv_obj_set_style_text_color(title, lv_color_black(), 0); @@ -48,16 +48,32 @@ class PowerOffApp final : public App { } } + static bool anyDeviceSupportsPowerOff() { + bool any_supported = false; + device_for_each_of_type(&POWER_SUPPLY_TYPE, &any_supported, [](Device* device, void* context) { + if (device_is_ready(device) && power_supply_supports_power_off(device)) { + *static_cast(context) = true; + return false; + } + return true; + }); + return any_supported; + } + static void onYesPressed(lv_event_t* /*event*/) { - auto* power = device_find_first_active_by_type(&POWER_SUPPLY_TYPE); - if (power == nullptr || !power_supply_supports_power_off(power)) { + if (!anyDeviceSupportsPowerOff()) { return; } auto display = hal::findFirstDevice(hal::Device::Type::Display); showPoweredOffScreenAndWait(display.get()); - power_supply_power_off(power); + device_for_each_of_type(&POWER_SUPPLY_TYPE, nullptr, [](Device* device, void* /*context*/) { + if (device_is_ready(device) && power_supply_supports_power_off(device)) { + power_supply_power_off(device); + } + return true; + }); } static void onNoPressed(lv_event_t* /*event*/) { @@ -66,7 +82,7 @@ class PowerOffApp final : public App { public: - void onShow(AppContext& /*app*/, lv_obj_t* parent) override { + void onShow(AppContext&, lv_obj_t* parent) override { lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_align(parent, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); @@ -77,7 +93,6 @@ class PowerOffApp final : public App { auto* button_wrapper = lv_obj_create(parent); lv_obj_set_flex_flow(button_wrapper, LV_FLEX_FLOW_ROW); lv_obj_set_size(button_wrapper, LV_SIZE_CONTENT, LV_SIZE_CONTENT); - lv_obj_set_style_pad_all(button_wrapper, 0, 0); lv_obj_set_style_border_width(button_wrapper, 0, 0); lv_obj_set_flex_align(button_wrapper, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); diff --git a/TactilityKernel/include/tactility/drivers/display.h b/TactilityKernel/include/tactility/drivers/display.h index 0355dd132..b9e35b99e 100644 --- a/TactilityKernel/include/tactility/drivers/display.h +++ b/TactilityKernel/include/tactility/drivers/display.h @@ -115,6 +115,24 @@ struct DisplayApi { */ error_t (*set_gap)(struct Device* device, int32_t x_gap, int32_t y_gap); + /** + * @brief Gets the devicetree-configured baseline X gap (i.e. as set at LV_DISPLAY_ROTATION_0, + * before any rotation-driven swap_xy/mirror changes) - not necessarily the last value passed + * to set_gap(), which callers (e.g. lvgl_display.c on rotation change) may have since swapped. + * @warning Function pointer should be null if capability not available. + * @param[in] device the display device + * @return the baseline X gap + */ + int32_t (*get_gap_x)(struct Device* device); + + /** + * @brief Gets the devicetree-configured baseline Y gap. See get_gap_x() for details. + * @warning Function pointer should be null if capability not available. + * @param[in] device the display device + * @return the baseline Y gap + */ + int32_t (*get_gap_y)(struct Device* device); + /** * @brief Inverts the panel's color output. * @warning Function pointer should be null if capability not available. @@ -250,6 +268,18 @@ bool display_get_mirror_y(struct Device* device); */ error_t display_set_gap(struct Device* device, int32_t x_gap, int32_t y_gap); +/** + * @brief Gets the devicetree-configured baseline X gap using the specified display. See + * DisplayApi::get_gap_x() for details. + */ +int32_t display_get_gap_x(struct Device* device); + +/** + * @brief Gets the devicetree-configured baseline Y gap using the specified display. See + * DisplayApi::get_gap_y() for details. + */ +int32_t display_get_gap_y(struct Device* device); + /** * @brief Inverts the panel's color output using the specified display. */ diff --git a/TactilityKernel/include/tactility/drivers/haptic.h b/TactilityKernel/include/tactility/drivers/haptic.h new file mode 100644 index 000000000..c0bccfe3b --- /dev/null +++ b/TactilityKernel/include/tactility/drivers/haptic.h @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#include +#include + +/** + * @brief API for haptic motor driver chips (e.g. ERM/LRA drivers with an onboard waveform + * library, such as the TI DRV260x family). + */ +struct HapticApi { + /** + * @brief Sets one slot of the effect sequence played by start_playback(). + * @param[in] device the haptic device + * @param[in] slot sequence slot index, driver-defined range (e.g. [0,7] for DRV260x) + * @param[in] waveform waveform library index to play in this slot, 0 ends the sequence + * @retval ERROR_NONE when the operation was successful + */ + error_t (*set_waveform)(struct Device* device, uint8_t slot, uint8_t waveform); + + /** + * @brief Selects the onboard waveform library to play effects from. + * @retval ERROR_NONE when the operation was successful + */ + error_t (*select_library)(struct Device* device, uint8_t library); + + /** + * @brief Starts playback of the currently configured effect sequence. + * @retval ERROR_NONE when the operation was successful + */ + error_t (*start_playback)(struct Device* device); + + /** + * @brief Stops playback. + * @retval ERROR_NONE when the operation was successful + */ + error_t (*stop_playback)(struct Device* device); +}; + +/** + * @brief Sets one slot of the effect sequence using the specified haptic device. + */ +error_t haptic_set_waveform(struct Device* device, uint8_t slot, uint8_t waveform); + +/** + * @brief Selects the onboard waveform library using the specified haptic device. + */ +error_t haptic_select_library(struct Device* device, uint8_t library); + +/** + * @brief Starts playback using the specified haptic device. + */ +error_t haptic_start_playback(struct Device* device); + +/** + * @brief Stops playback using the specified haptic device. + */ +error_t haptic_stop_playback(struct Device* device); + +extern const struct DeviceType HAPTIC_TYPE; + +#ifdef __cplusplus +} +#endif diff --git a/TactilityKernel/include/tactility/drivers/pointer.h b/TactilityKernel/include/tactility/drivers/pointer.h index f346ac502..284224c7c 100644 --- a/TactilityKernel/include/tactility/drivers/pointer.h +++ b/TactilityKernel/include/tactility/drivers/pointer.h @@ -17,15 +17,19 @@ extern "C" { struct PointerApi { /** * @brief Puts the pointer controller into its low-power sleep mode. + * @warning Function pointer should be null if not supported by the underlying protocol. * @param[in] device the pointer device * @retval ERROR_NONE when the operation was successful + * @retval ERROR_NOT_SUPPORTED when this controller has no sleep mode */ error_t (*enter_sleep)(struct Device* device); /** * @brief Wakes the pointer controller from sleep mode. + * @warning Function pointer should be null if not supported by the underlying protocol. * @param[in] device the pointer device * @retval ERROR_NONE when the operation was successful + * @retval ERROR_NOT_SUPPORTED when this controller has no sleep mode */ error_t (*exit_sleep)(struct Device* device); diff --git a/TactilityKernel/source/drivers/display.cpp b/TactilityKernel/source/drivers/display.cpp index d8ae083a9..ab0830981 100644 --- a/TactilityKernel/source/drivers/display.cpp +++ b/TactilityKernel/source/drivers/display.cpp @@ -60,6 +60,18 @@ error_t display_set_gap(Device* device, int32_t x_gap, int32_t y_gap) { return DISPLAY_DRIVER_API(driver)->set_gap(device, x_gap, y_gap); } +int32_t display_get_gap_x(Device* device) { + const auto* driver = device_get_driver(device); + const auto* api = DISPLAY_DRIVER_API(driver); + return api->get_gap_x != nullptr ? api->get_gap_x(device) : 0; +} + +int32_t display_get_gap_y(Device* device) { + const auto* driver = device_get_driver(device); + const auto* api = DISPLAY_DRIVER_API(driver); + return api->get_gap_y != nullptr ? api->get_gap_y(device) : 0; +} + error_t display_invert_color(Device* device, bool invert_color_data) { const auto* driver = device_get_driver(device); return DISPLAY_DRIVER_API(driver)->invert_color(device, invert_color_data); @@ -102,7 +114,11 @@ uint8_t display_get_frame_buffer_count(Device* device) { error_t display_get_backlight(Device* device, Device** backlight) { const auto* driver = device_get_driver(device); - return DISPLAY_DRIVER_API(driver)->get_backlight(device, backlight); + const auto* api = DISPLAY_DRIVER_API(driver); + if (api->get_backlight == nullptr) { + return ERROR_NOT_SUPPORTED; + } + return api->get_backlight(device, backlight); } const struct DeviceType DISPLAY_TYPE { diff --git a/TactilityKernel/source/drivers/haptic.cpp b/TactilityKernel/source/drivers/haptic.cpp new file mode 100644 index 000000000..6033a8a14 --- /dev/null +++ b/TactilityKernel/source/drivers/haptic.cpp @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include + +#define HAPTIC_DRIVER_API(driver) ((HapticApi*)driver->api) + +extern "C" { + +error_t haptic_set_waveform(Device* device, uint8_t slot, uint8_t waveform) { + const auto* driver = device_get_driver(device); + return HAPTIC_DRIVER_API(driver)->set_waveform(device, slot, waveform); +} + +error_t haptic_select_library(Device* device, uint8_t library) { + const auto* driver = device_get_driver(device); + return HAPTIC_DRIVER_API(driver)->select_library(device, library); +} + +error_t haptic_start_playback(Device* device) { + const auto* driver = device_get_driver(device); + return HAPTIC_DRIVER_API(driver)->start_playback(device); +} + +error_t haptic_stop_playback(Device* device) { + const auto* driver = device_get_driver(device); + return HAPTIC_DRIVER_API(driver)->stop_playback(device); +} + +const DeviceType HAPTIC_TYPE { + .name = "haptic" +}; + +} diff --git a/TactilityKernel/source/drivers/pointer.cpp b/TactilityKernel/source/drivers/pointer.cpp index 33d609c53..f9cc9d108 100644 --- a/TactilityKernel/source/drivers/pointer.cpp +++ b/TactilityKernel/source/drivers/pointer.cpp @@ -8,12 +8,20 @@ extern "C" { error_t pointer_enter_sleep(struct Device* device) { const auto* driver = device_get_driver(device); - return POINTER_DRIVER_API(driver)->enter_sleep(device); + const auto* api = POINTER_DRIVER_API(driver); + if (api->enter_sleep == nullptr) { + return ERROR_NOT_SUPPORTED; + } + return api->enter_sleep(device); } error_t pointer_exit_sleep(Device* device) { const auto* driver = device_get_driver(device); - return POINTER_DRIVER_API(driver)->exit_sleep(device); + const auto* api = POINTER_DRIVER_API(driver); + if (api->exit_sleep == nullptr) { + return ERROR_NOT_SUPPORTED; + } + return api->exit_sleep(device); } error_t pointer_read_data(Device* device, TickType_t timeout) { From 24e46d60d04c4c847629d38f65ee412beed49513 Mon Sep 17 00:00:00 2001 From: Ken Van Hoeylandt Date: Sat, 18 Jul 2026 11:49:22 +0200 Subject: [PATCH 2/4] Fix for display --- .../source/axs15231b_display.cpp | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/Drivers/axs15231b-module/source/axs15231b_display.cpp b/Drivers/axs15231b-module/source/axs15231b_display.cpp index 76830668f..df6a60f5e 100644 --- a/Drivers/axs15231b-module/source/axs15231b_display.cpp +++ b/Drivers/axs15231b-module/source/axs15231b_display.cpp @@ -69,9 +69,7 @@ static gpio_num_t pin_or_nc(const struct GpioPinSpec& pin) { // 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/axs,axs15231b.yaml) into a heap-allocated axs15231b_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. +// bindings/axs,axs15231b.yaml) into a heap-allocated axs15231b_lcd_init_cmd_t array. static bool parse_init_sequence(const uint8_t* bytes, uint32_t length, axs15231b_lcd_init_cmd_t** out_cmds, uint16_t* out_count) { uint32_t count = 0; for (uint32_t offset = 0; offset < length; count++) { @@ -283,17 +281,23 @@ static error_t start(Device* device) { return ERROR_RESOURCE; } - // Bring-up sequence. swap_xy is intentionally not called (and not exposed in DisplayApi - // below): it doesn't work on this chip/panel combination (confirmed on real hardware by the - // original deprecated-HAL driver). Every failure path below 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. + // Bring-up sequence. swap_xy is intentionally not called (and not exposed in DisplayApi below): + // It doesn't work on this chip/panel combination. + // + // esp_lcd_axs15231b's disp_on_off callback is wired up backwards: its body branches on a + // parameter it names "off" (true -> DISPOFF, false -> DISPON), but esp_lcd_panel_disp_on_off() + // forwards its "on_off" argument straight through with no inversion - so passing true here + // actually switches the panel OFF. Pass false to really turn it on (confirmed against the + // deleted deprecated-HAL driver, which called this same function with false for the same + // reason). See axs15231b_disp_on_off() below, which un-inverts this for DisplayApi callers. + // + // (note: all of this was tested on guition-jc3248w535c only) bool ok = esp_lcd_panel_reset(internal->panel_handle) == ESP_OK && esp_lcd_panel_init(internal->panel_handle) == ESP_OK && esp_lcd_panel_mirror(internal->panel_handle, config->mirror_x, config->mirror_y) == ESP_OK && esp_lcd_panel_invert_color(internal->panel_handle, config->invert_color) == ESP_OK && - esp_lcd_panel_disp_on_off(internal->panel_handle, true) == ESP_OK; + esp_lcd_panel_disp_on_off(internal->panel_handle, false) == ESP_OK; if (!ok) { LOG_E(TAG, "Failed to bring up panel"); @@ -404,7 +408,8 @@ static error_t axs15231b_invert_color(Device* device, bool invert_color_data) { static error_t axs15231b_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; + // Inverted: see the comment on the disp_on_off call in start() above. + return esp_lcd_panel_disp_on_off(internal->panel_handle, !on_off) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE; } // _SWAPPED (not plain RGB565): the panel expects each 16-bit pixel high-byte-first over the QSPI From 863806eba5c8cb26373c7e6c819dd7d2aea1d4bd Mon Sep 17 00:00:00 2001 From: Ken Van Hoeylandt Date: Sat, 18 Jul 2026 13:54:13 +0200 Subject: [PATCH 3/4] Driver improvements --- Drivers/axs15231b-module/source/axs15231b_display.cpp | 8 +++++++- Modules/lvgl-module/include/tactility/lvgl_display.h | 9 +++++++++ Modules/lvgl-module/source/lvgl_devices.c | 3 ++- Modules/lvgl-module/source/lvgl_display.c | 5 +++-- TactilityKernel/include/tactility/drivers/display.h | 9 ++++++++- 5 files changed, 29 insertions(+), 5 deletions(-) diff --git a/Drivers/axs15231b-module/source/axs15231b_display.cpp b/Drivers/axs15231b-module/source/axs15231b_display.cpp index df6a60f5e..37e2fcf3f 100644 --- a/Drivers/axs15231b-module/source/axs15231b_display.cpp +++ b/Drivers/axs15231b-module/source/axs15231b_display.cpp @@ -448,8 +448,14 @@ static error_t axs15231b_get_backlight(Device* device, Device** backlight) { // endregion static const DisplayApi axs15231b_display_api = { + // REQUIRES_FULL_FRAME: this panel's QSPI command set has no row-address (RASET) command at + // all (see draw_bitmap() above) - only a column range (CASET) plus a RAMWR/RAMWRC choice that + // resets vs. continues an internal row auto-increment counter. A sub-region write leaves that + // counter unsynchronized with the logical area actually intended, which is what produced the + // sheared/stretched image before this flag existed (the deleted deprecated-HAL driver worked + // around the same limitation by unconditionally flushing the full frame on every redraw). .capabilities = DISPLAY_CAPABILITY_CAP_MIRROR | DISPLAY_CAPABILITY_INVERT_COLOR | - DISPLAY_CAPABILITY_ON_OFF | DISPLAY_CAPABILITY_BACKLIGHT, + DISPLAY_CAPABILITY_ON_OFF | DISPLAY_CAPABILITY_BACKLIGHT | DISPLAY_CAPABILITY_REQUIRES_FULL_FRAME, .reset = axs15231b_reset, .init = axs15231b_init, .draw_bitmap = axs15231b_draw_bitmap, diff --git a/Modules/lvgl-module/include/tactility/lvgl_display.h b/Modules/lvgl-module/include/tactility/lvgl_display.h index c3b396a02..854de130e 100644 --- a/Modules/lvgl-module/include/tactility/lvgl_display.h +++ b/Modules/lvgl-module/include/tactility/lvgl_display.h @@ -44,6 +44,15 @@ struct LvglDisplayConfig { * RGB565/BGR565 (e.g. RGB888, MONOCHROME). */ bool swap_bytes; + + /** + * Forces LV_DISPLAY_RENDER_MODE_FULL with a full-resolution buffer, ignoring buffer_height, + * and always flushes the entire display rather than per-tile dirty regions. Set this when the + * device reports DISPLAY_CAPABILITY_REQUIRES_FULL_FRAME - see that capability's doc comment. + * Ignored when the device exposes its own frame buffer(s) (already always-full-frame) or uses + * the LV_COLOR_FORMAT_I1 path (already always-full-frame). + */ + bool force_full_frame; }; /** diff --git a/Modules/lvgl-module/source/lvgl_devices.c b/Modules/lvgl-module/source/lvgl_devices.c index 10a5cc170..2d81beb8f 100644 --- a/Modules/lvgl-module/source/lvgl_devices.c +++ b/Modules/lvgl-module/source/lvgl_devices.c @@ -45,7 +45,8 @@ void lvgl_devices_attach() { color_format == DISPLAY_COLOR_FORMAT_BGR565; struct LvglDisplayConfig lvgl_display_config = { .buffer_height = vres > 10 ? vres / 10 : vres, - .swap_bytes = swap_bytes + .swap_bytes = swap_bytes, + .force_full_frame = display_has_capability(kernel_display_device, DISPLAY_CAPABILITY_REQUIRES_FULL_FRAME) }; if (lvgl_display_add(kernel_display_device, &lvgl_display_config, &lvgl_display) == ERROR_NONE) { LOG_I(TAG, "Bound %s to LVGL", kernel_display_device->name); diff --git a/Modules/lvgl-module/source/lvgl_display.c b/Modules/lvgl-module/source/lvgl_display.c index 3447b3361..48266ae91 100644 --- a/Modules/lvgl-module/source/lvgl_display.c +++ b/Modules/lvgl-module/source/lvgl_display.c @@ -319,7 +319,8 @@ error_t lvgl_display_add(struct Device* device, const struct LvglDisplayConfig* render_mode = LV_DISPLAY_RENDER_MODE_FULL; buf_size_bytes = (size_t)hres * vres * bpp; } else { - uint16_t buf_height = config->buffer_height > 0 ? config->buffer_height : vres; + uint16_t buf_height = config->force_full_frame || config->buffer_height == 0 + ? vres : config->buffer_height; buf_size_bytes = (size_t)hres * buf_height * bpp; ctx->buf1 = lvgl_display_alloc_buffer(buf_size_bytes); @@ -336,7 +337,7 @@ error_t lvgl_display_add(struct Device* device, const struct LvglDisplayConfig* } } ctx->owns_buffers = true; - render_mode = LV_DISPLAY_RENDER_MODE_PARTIAL; + render_mode = config->force_full_frame ? LV_DISPLAY_RENDER_MODE_FULL : LV_DISPLAY_RENDER_MODE_PARTIAL; } ctx->buf_size_bytes = buf_size_bytes; diff --git a/TactilityKernel/include/tactility/drivers/display.h b/TactilityKernel/include/tactility/drivers/display.h index b9e35b99e..86aec4d64 100644 --- a/TactilityKernel/include/tactility/drivers/display.h +++ b/TactilityKernel/include/tactility/drivers/display.h @@ -20,7 +20,14 @@ enum DisplayCapability { DISPLAY_CAPABILITY_INVERT_COLOR = 1 << 3, DISPLAY_CAPABILITY_ON_OFF = 1 << 4, DISPLAY_CAPABILITY_BACKLIGHT = 1 << 5, - DISPLAY_CAPABILITY_SLEEP = 1 << 6 + DISPLAY_CAPABILITY_SLEEP = 1 << 6, + /** + * Panel can only be written a full frame at a time (e.g. no row-address command, relying on + * an internal auto-increment counter reset by writing the whole frame from the top every + * time) - draw_bitmap() must always be called with the full resolution, never a sub-region. + * LVGL-facing callers (see lvgl_display.c) must use LV_DISPLAY_RENDER_MODE_FULL for such panels. + */ + DISPLAY_CAPABILITY_REQUIRES_FULL_FRAME = 1 << 7 }; /** From fe9d758dc3b0b1203df8573e7a754b84e0f0ac42 Mon Sep 17 00:00:00 2001 From: Ken Van Hoeylandt Date: Sat, 18 Jul 2026 14:21:51 +0200 Subject: [PATCH 4/4] Move requires-full-frame to dts --- .../guition,jc3248w535c.dts | 4 +++ .../bindings/axs,axs15231b.yaml | 8 ++++++ .../include/drivers/axs15231b_display.h | 2 ++ .../source/axs15231b_display.cpp | 28 +++++++++++++------ 4 files changed, 34 insertions(+), 8 deletions(-) diff --git a/Devices/guition-jc3248w535c/guition,jc3248w535c.dts b/Devices/guition-jc3248w535c/guition,jc3248w535c.dts index 39cda474b..d820fdb78 100644 --- a/Devices/guition-jc3248w535c/guition,jc3248w535c.dts +++ b/Devices/guition-jc3248w535c/guition,jc3248w535c.dts @@ -90,6 +90,10 @@ vertical-resolution = <480>; pin-te = <&gpio0 38 GPIO_FLAG_NONE>; backlight = <&display_backlight>; + // Confirmed on real hardware: a sub-region draw desyncs this panel's row + // auto-increment counter (no QSPI row-address command exists to resync it), + // producing a sheared/doubled image - see requires-full-frame's binding doc. + requires-full-frame; // Vendor bring-up sequence, carried over unchanged from the deprecated HAL's old // Axs15231bDisplay.cpp. Framing is [cmd, data-length, delay-ms, data-length bytes of diff --git a/Drivers/axs15231b-module/bindings/axs,axs15231b.yaml b/Drivers/axs15231b-module/bindings/axs,axs15231b.yaml index 0ee27f5ca..1ea3cacc1 100644 --- a/Drivers/axs15231b-module/bindings/axs,axs15231b.yaml +++ b/Drivers/axs15231b-module/bindings/axs,axs15231b.yaml @@ -61,6 +61,14 @@ properties: Custom vendor bring-up sequence, flattened into bytes as a run of [cmd, data-length, delay-ms, data-length bytes of data...] entries. Omit to use the AXS15231B component's own built-in default sequence. + requires-full-frame: + type: boolean + default: false + description: > + Whether this panel needs full-frame-only draws (DISPLAY_CAPABILITY_REQUIRES_FULL_FRAME) - + a sub-region draw_bitmap() call desyncs this chip's row auto-increment counter, since its + QSPI command set has no row-address command. + It's not certain that this is required for all driver implementations, so it's a config option for now. backlight: type: phandle default: "NULL" diff --git a/Drivers/axs15231b-module/include/drivers/axs15231b_display.h b/Drivers/axs15231b-module/include/drivers/axs15231b_display.h index 77f353083..98489e3c9 100644 --- a/Drivers/axs15231b-module/include/drivers/axs15231b_display.h +++ b/Drivers/axs15231b-module/include/drivers/axs15231b_display.h @@ -37,6 +37,8 @@ struct Axs15231bDisplayConfig { const uint8_t* init_sequence; uint32_t init_sequence_length; + bool requires_full_frame; + // Optional reference to this display's backlight device, NULL if none. struct Device* backlight; }; diff --git a/Drivers/axs15231b-module/source/axs15231b_display.cpp b/Drivers/axs15231b-module/source/axs15231b_display.cpp index 37e2fcf3f..732977ea5 100644 --- a/Drivers/axs15231b-module/source/axs15231b_display.cpp +++ b/Drivers/axs15231b-module/source/axs15231b_display.cpp @@ -445,17 +445,29 @@ static error_t axs15231b_get_backlight(Device* device, Device** backlight) { return ERROR_NONE; } +// REQUIRES_FULL_FRAME is the only capability that varies per device instance (via the +// requires-full-frame devicetree property, see axs15231b_display.h) - a sub-region draw_bitmap() +// call desyncs this chip's row auto-increment counter, since its QSPI command set has no +// row-address command, but that's been confirmed needed on some boards' wiring/panel combination +// and not assumed true for every AXS15231B board (see start()'s notes on what's been tested where). +// Every other bit stays fixed for the driver, mirrored here from axs15231b_display_api.capabilities. +static bool axs15231b_has_capability(Device* device, uint32_t capability) { + uint32_t static_capabilities = DISPLAY_CAPABILITY_CAP_MIRROR | DISPLAY_CAPABILITY_INVERT_COLOR | + DISPLAY_CAPABILITY_ON_OFF | DISPLAY_CAPABILITY_BACKLIGHT; + if (GET_CONFIG(device)->requires_full_frame) { + static_capabilities |= DISPLAY_CAPABILITY_REQUIRES_FULL_FRAME; + } + return (static_capabilities & capability) == capability; +} + // endregion static const DisplayApi axs15231b_display_api = { - // REQUIRES_FULL_FRAME: this panel's QSPI command set has no row-address (RASET) command at - // all (see draw_bitmap() above) - only a column range (CASET) plus a RAMWR/RAMWRC choice that - // resets vs. continues an internal row auto-increment counter. A sub-region write leaves that - // counter unsynchronized with the logical area actually intended, which is what produced the - // sheared/stretched image before this flag existed (the deleted deprecated-HAL driver worked - // around the same limitation by unconditionally flushing the full frame on every redraw). + // Mirrors axs15231b_has_capability()'s static_capabilities for callers that read this field + // directly instead of going through display_has_capability()/has_capability(). Excludes + // REQUIRES_FULL_FRAME - see axs15231b_has_capability() above, which is the source of truth. .capabilities = DISPLAY_CAPABILITY_CAP_MIRROR | DISPLAY_CAPABILITY_INVERT_COLOR | - DISPLAY_CAPABILITY_ON_OFF | DISPLAY_CAPABILITY_BACKLIGHT | DISPLAY_CAPABILITY_REQUIRES_FULL_FRAME, + DISPLAY_CAPABILITY_ON_OFF | DISPLAY_CAPABILITY_BACKLIGHT, .reset = axs15231b_reset, .init = axs15231b_init, .draw_bitmap = axs15231b_draw_bitmap, @@ -474,7 +486,7 @@ static const DisplayApi axs15231b_display_api = { .get_frame_buffer = axs15231b_get_frame_buffer, .get_frame_buffer_count = axs15231b_get_frame_buffer_count, .get_backlight = axs15231b_get_backlight, - .has_capability = nullptr, + .has_capability = axs15231b_has_capability, }; Driver axs15231b_display_driver = {