From c6373b79e95a54874861a7df39b2b6b474c1758e Mon Sep 17 00:00:00 2001 From: NellowTCS Date: Fri, 7 Aug 2026 11:30:22 -0600 Subject: [PATCH 1/3] Implement CL-32 Support (#610) --- .gitmodules | 3 + CMakeLists.txt | 1 + Devices/cl32/CMakeLists.txt | 7 + Devices/cl32/LICENSE-Apache-2.0.md | 195 ++++++++++ Devices/cl32/cl32.dts | 121 ++++++ Devices/cl32/device.properties | 26 ++ Devices/cl32/devicetree.yaml | 6 + Devices/cl32/source/module.cpp | 9 + Drivers/esp-epaper-module/CMakeLists.txt | 9 + .../esp-epaper-module/LICENSE-Apache-2.0.md | 195 ++++++++++ .../bindings/tuanpmt,esp-epaper.yaml | 65 ++++ Drivers/esp-epaper-module/devicetree.yaml | 3 + .../include/bindings/esp_epaper.h | 7 + .../include/drivers/esp_epaper.h | 44 +++ .../include/esp_epaper_module.h | 14 + .../esp-epaper-module/source/esp_epaper.cpp | 353 ++++++++++++++++++ .../source/esp_epaper_rotate.h | 69 ++++ Drivers/esp-epaper-module/source/module.cpp | 19 + Libraries/esp_epaper | 1 + Tactility/Source/lvgl/UsbHidInput.cpp | 18 +- device.py | 5 + partitions-16mb-no-sd-dev.csv | 2 +- partitions-16mb-no-sd.csv | 2 +- partitions-16mb-with-sd.csv | 2 +- partitions-4mb-with-sd.csv | 2 +- partitions-8mb-no-sd-dev.csv | 2 +- partitions-8mb-no-sd.csv | 2 +- partitions-8mb-with-sd.csv | 2 +- 28 files changed, 1172 insertions(+), 12 deletions(-) create mode 100644 Devices/cl32/CMakeLists.txt create mode 100644 Devices/cl32/LICENSE-Apache-2.0.md create mode 100644 Devices/cl32/cl32.dts create mode 100644 Devices/cl32/device.properties create mode 100644 Devices/cl32/devicetree.yaml create mode 100644 Devices/cl32/source/module.cpp create mode 100644 Drivers/esp-epaper-module/CMakeLists.txt create mode 100644 Drivers/esp-epaper-module/LICENSE-Apache-2.0.md create mode 100644 Drivers/esp-epaper-module/bindings/tuanpmt,esp-epaper.yaml create mode 100644 Drivers/esp-epaper-module/devicetree.yaml create mode 100644 Drivers/esp-epaper-module/include/bindings/esp_epaper.h create mode 100644 Drivers/esp-epaper-module/include/drivers/esp_epaper.h create mode 100644 Drivers/esp-epaper-module/include/esp_epaper_module.h create mode 100644 Drivers/esp-epaper-module/source/esp_epaper.cpp create mode 100644 Drivers/esp-epaper-module/source/esp_epaper_rotate.h create mode 100644 Drivers/esp-epaper-module/source/module.cpp create mode 160000 Libraries/esp_epaper diff --git a/.gitmodules b/.gitmodules index 0d18c27c6..695474766 100644 --- a/.gitmodules +++ b/.gitmodules @@ -16,3 +16,6 @@ [submodule "Libraries/cJSON/cJSON"] path = Libraries/cJSON/cJSON url = https://github.com/DaveGamble/cJSON.git +[submodule "Libraries/esp_epaper"] + path = Libraries/esp_epaper + url = https://github.com/NellowTCS/esp_epaper.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 02965b4be..195f180a2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -45,6 +45,7 @@ if (DEFINED ENV{ESP_IDF_VERSION}) "TactilityC" "TactilityFreeRtos" "Libraries/elf_loader" + "Libraries/esp_epaper" "Libraries/lv_screenshot" "Libraries/minitar" "Libraries/minmea" diff --git a/Devices/cl32/CMakeLists.txt b/Devices/cl32/CMakeLists.txt new file mode 100644 index 000000000..09a2fdee3 --- /dev/null +++ b/Devices/cl32/CMakeLists.txt @@ -0,0 +1,7 @@ +file(GLOB_RECURSE SOURCE_FILES source/*.c*) + +idf_component_register( + SRCS ${SOURCE_FILES} + INCLUDE_DIRS "source" + REQUIRES TactilityKernel +) diff --git a/Devices/cl32/LICENSE-Apache-2.0.md b/Devices/cl32/LICENSE-Apache-2.0.md new file mode 100644 index 000000000..f5f4b8b5e --- /dev/null +++ b/Devices/cl32/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/Devices/cl32/cl32.dts b/Devices/cl32/cl32.dts new file mode 100644 index 000000000..9027c8e99 --- /dev/null +++ b/Devices/cl32/cl32.dts @@ -0,0 +1,121 @@ +/dts-v1/; + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +/ { + compatible = "root"; + model = "CL-32"; + + gpio0 { + compatible = "espressif,esp32-gpio"; + gpio-count = <49>; + }; + + wifi0 { + compatible = "espressif,esp32-wifi"; + status = "disabled"; + }; + + // Top Stemma/Qwiic port 1 shares this bus with the keyboard and RTC (SDA: pin 1 <-> SCL: pin 2). + // External I2C devices on that port are reachable via i2c0. + i2c0 { + compatible = "espressif,esp32-i2c"; + port = ; + clock-frequency = <100000>; + pin-sda = <&gpio0 1 GPIO_FLAG_PULL_UP>; + pin-scl = <&gpio0 2 GPIO_FLAG_PULL_UP>; + + keyboard { + compatible = "ti,tca8418"; + reg = <0x34>; + rows = <8>; + columns = <10>; + keymap-lc = [ + 37 49 50 51 52 53 54 55 56 0 // % 1 2 3 4 5 6 7 8 + 57 48 8 91 93 43 34 39 27 0 // 9 0 BKSP [ ] + " ' EXIT + 9 113 119 101 114 116 121 117 105 0 // TAB q w e r t y u i + 111 112 10 40 41 45 59 58 3 0 // o p ENTER ( ) - ; : STOP + 0 97 115 100 102 103 104 106 107 0 // a s d f g h j k + 108 17 35 123 125 42 44 46 2 0 // l UP # { } * , . MENU + 122 120 99 118 98 32 32 110 109 0 // z x c v b n m + 20 18 19 60 62 47 92 61 10 0 // LEFT DOWN RIGHT < > / \ = RUN + ]; + keymap-uc = [ + 37 49 50 51 52 53 54 55 56 0 // % 1 2 3 4 5 6 7 8 + 57 48 8 91 93 43 34 39 27 0 // 9 0 BKSP [ ] + " ' EXIT + 9 81 87 69 82 84 89 85 73 0 // TAB Q W E R T Y U I + 79 80 10 40 41 45 59 58 3 0 // O P ENTER ( ) - ; : STOP + 0 65 83 68 70 71 72 74 75 0 // A S D F G H J K + 76 17 35 123 125 42 44 46 2 0 // L UP # { } * , . MENU + 90 88 67 86 66 32 32 78 77 0 // Z X C V B N M + 20 18 19 60 62 47 92 61 10 0 // LEFT DOWN RIGHT < > / \ = RUN + ]; + keymap-sy = [ + 37 49 50 51 52 53 54 55 56 0 // % 1 2 3 4 5 6 7 8 + 57 48 8 91 93 43 34 39 27 0 // 9 0 BKSP [ ] + " ' EXIT + 9 113 119 101 114 116 121 117 105 0 // TAB q w e r t y u i + 111 112 10 40 41 45 59 58 3 0 // o p ENTER ( ) - ; : STOP + 0 97 115 100 102 103 104 106 107 0 // a s d f g h j k + 108 17 35 123 125 42 44 46 2 0 // l UP # { } * , . MENU + 122 120 99 118 98 32 32 110 109 0 // z x c v b n m + 20 18 19 60 62 47 92 61 10 0 // LEFT DOWN RIGHT < > / \ = RUN + ]; + shift-row = <4>; + shift-col = <0>; + sym-row = <5>; + sym-col = <8>; + }; + + rtc: bm8563 { + compatible = "belling,bm8563"; + reg = <0x51>; + }; + }; + + port_b: grove1 { + compatible = "espressif,esp32-grove"; + defaultMode = ; + pinSdaTx = <&gpio0 47 GPIO_FLAG_NONE>; + pinSclRx = <&gpio0 48 GPIO_FLAG_NONE>; + uartPort = ; + i2cPort = ; + i2cClockFrequency = <100000>; + }; + + spi1 { + compatible = "espressif,esp32-spi"; + host = ; + cs-gpios = <&gpio0 7 GPIO_FLAG_NONE>; + pin-mosi = <&gpio0 10 GPIO_FLAG_NONE>; + pin-miso = <&gpio0 11 GPIO_FLAG_NONE>; + pin-sclk = <&gpio0 9 GPIO_FLAG_NONE>; + max-transfer-size = <4096>; + + sdcard@0 { + compatible = "espressif,esp32-sdspi"; + frequency-khz = <20000>; + }; + + epd@1 { + compatible = "tuanpmt,esp-epaper"; + pin-dc = <&gpio0 13 GPIO_FLAG_NONE>; + pin-reset = <&gpio0 12 GPIO_FLAG_NONE>; + pin-busy = <&gpio0 14 GPIO_FLAG_NONE>; + pin-cs = <&gpio0 6 GPIO_FLAG_NONE>; + clock-speed-hz = <4000000>; + panel-type = "gdey029t71h"; + update-mode = <2>; + rotation = <1>; + }; + }; +}; diff --git a/Devices/cl32/device.properties b/Devices/cl32/device.properties new file mode 100644 index 000000000..f59540f82 --- /dev/null +++ b/Devices/cl32/device.properties @@ -0,0 +1,26 @@ +general.vendor=CL-32 +general.name=CL-32 + +apps.launcherAppId=Launcher + +hardware.target=ESP32S3 +hardware.flashSize=8MB +hardware.spiRam=true +hardware.spiRamMode=QUAD +hardware.spiRamSpeed=80M +hardware.esptoolFlashFreq=80M +hardware.bluetooth=true + +storage.userDataLocation=SD + +display.size=2.9" +display.shape=rectangle +display.dpi=139 + +lvgl.colorDepth=8 +lvgl.theme=Mono + +# Fix error "PSRAM space not enough for the Flash instructions" on boot: +sdkconfig.CONFIG_SPIRAM_FETCH_INSTRUCTIONS=n +sdkconfig.CONFIG_SPIRAM_RODATA=n +sdkconfig.CONFIG_SPIRAM_XIP_FROM_PSRAM=n diff --git a/Devices/cl32/devicetree.yaml b/Devices/cl32/devicetree.yaml new file mode 100644 index 000000000..e2bc7d3bb --- /dev/null +++ b/Devices/cl32/devicetree.yaml @@ -0,0 +1,6 @@ +dependencies: + - Platforms/platform-esp32 + - Drivers/tca8418-module + - Drivers/esp-epaper-module + - Drivers/bm8563-module +dts: cl32.dts diff --git a/Devices/cl32/source/module.cpp b/Devices/cl32/source/module.cpp new file mode 100644 index 000000000..5b88b4dd5 --- /dev/null +++ b/Devices/cl32/source/module.cpp @@ -0,0 +1,9 @@ +#include + +extern "C" { + +struct Module cl32_module = { + .name = "cl32" +}; + +} diff --git a/Drivers/esp-epaper-module/CMakeLists.txt b/Drivers/esp-epaper-module/CMakeLists.txt new file mode 100644 index 000000000..7bf6aa3a5 --- /dev/null +++ b/Drivers/esp-epaper-module/CMakeLists.txt @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION 3.20) + +include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake") + +tactility_add_module(esp-epaper-module + SRCS source/esp_epaper.cpp source/module.cpp + INCLUDE_DIRS include/ + REQUIRES TactilityKernel platform-esp32 driver esp_epaper +) diff --git a/Drivers/esp-epaper-module/LICENSE-Apache-2.0.md b/Drivers/esp-epaper-module/LICENSE-Apache-2.0.md new file mode 100644 index 000000000..f5f4b8b5e --- /dev/null +++ b/Drivers/esp-epaper-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/esp-epaper-module/bindings/tuanpmt,esp-epaper.yaml b/Drivers/esp-epaper-module/bindings/tuanpmt,esp-epaper.yaml new file mode 100644 index 000000000..8b44b5e4a --- /dev/null +++ b/Drivers/esp-epaper-module/bindings/tuanpmt,esp-epaper.yaml @@ -0,0 +1,65 @@ +description: > + E-paper display panels driven by the tuanpmt/esp_epaper ESP-IDF component + (SSD1680/1681 BW, GDEY0154D67 and ACeP/BWRY color controllers). The node must + be a child of an SPI controller: the display shares the parent's SPI host, + clock and MOSI pins, and manages its DC/RESET/BUSY/CS pins itself via the + component. Only 1bpp black/white panels are supported by the kernel display + bridge (DISPLAY_COLOR_FORMAT_MONOCHROME); color panels fail to start. + +compatible: "tuanpmt,esp-epaper" + +bus: spi + +properties: + pin-dc: + type: phandles + required: true + description: Data/Command GPIO pin + pin-reset: + type: phandles + required: true + description: Reset GPIO pin (the esp_epaper component drives it unconditionally) + pin-busy: + type: phandles + required: true + description: Busy GPIO pin (active high) + pin-cs: + type: phandles + required: true + description: Chip-select GPIO pin (driven manually by the component) + clock-speed-hz: + type: int + default: 4000000 + description: SPI clock frequency in Hz (must be > 0) + panel-type: + type: text + required: true + description: > + esp_epaper panel registry name. One of "gdey0154d67", "gdep073e01", + "gdey037f51", "gdey029t71h", "ssd16xx-154", "ssd16xx-213", "ssd16xx-266", + "ssd16xx-270", "ssd16xx-290", "ssd16xx-370", "ssd16xx-420". + width: + type: int + default: 0 + description: Horizontal resolution override in pixels (0 = the panel's default, maximum 2048) + height: + type: int + default: 0 + description: Vertical resolution override in pixels (0 = the panel's default, maximum 2048) + update-mode: + type: int + default: 0 + description: > + epd_update_mode_t used for frame updates: 0 = full, 1 = fast, 2 = partial. + Full is the safest default; fast and partial refresh quickly but ghost. + Values above 2 are rejected at driver start. + rotation: + type: int + default: 0 + description: > + Fixed display rotation applied at start. 0 = 0 degrees, 1 = 90 degrees, + 2 = 180 degrees, 3 = 270 degrees counter-clockwise (matching + LV_DISPLAY_ROTATION_*). The kernel bridge reports the rotated resolution, + so LVGL renders in rotated space and the driver rotates the 1bpp frame + back to the panel's native layout before updating. Not changeable at + runtime. diff --git a/Drivers/esp-epaper-module/devicetree.yaml b/Drivers/esp-epaper-module/devicetree.yaml new file mode 100644 index 000000000..a07d6f334 --- /dev/null +++ b/Drivers/esp-epaper-module/devicetree.yaml @@ -0,0 +1,3 @@ +dependencies: + - TactilityKernel +bindings: bindings diff --git a/Drivers/esp-epaper-module/include/bindings/esp_epaper.h b/Drivers/esp-epaper-module/include/bindings/esp_epaper.h new file mode 100644 index 000000000..0c6057f61 --- /dev/null +++ b/Drivers/esp-epaper-module/include/bindings/esp_epaper.h @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include + +DEFINE_DEVICETREE(esp_epaper, struct EspEpaperConfig) diff --git a/Drivers/esp-epaper-module/include/drivers/esp_epaper.h b/Drivers/esp-epaper-module/include/drivers/esp_epaper.h new file mode 100644 index 000000000..cba3a7706 --- /dev/null +++ b/Drivers/esp-epaper-module/include/drivers/esp_epaper.h @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#include +#include + +/** + * @brief Panel configuration for an esp_epaper-driven e-paper display. + * + * The struct field order matches the binding's property order and must stay in + * sync with it: the devicetree compiler emits struct initializers in binding + * order, not declaration order. + */ +struct EspEpaperConfig { + struct GpioPinSpec pin_dc; + struct GpioPinSpec pin_reset; + struct GpioPinSpec pin_busy; + struct GpioPinSpec pin_cs; + /** SPI clock frequency in Hz */ + int clock_speed_hz; + /** Panel registry name, e.g. "ssd16xx-290" (see the binding's panel-type) */ + const char* panel_type; + /** Horizontal resolution override; 0 = the panel's default */ + uint16_t width; + /** Vertical resolution override; 0 = the panel's default */ + uint16_t height; + /** epd_update_mode_t used for frame updates */ + epd_update_mode_t update_mode; + /** + * Fixed display rotation, 0 = 0 degrees, 1 = 90, 2 = 180, 3 = 270 degrees + * counter-clockwise (LV_DISPLAY_ROTATION_*). Not changeable at runtime. + */ + uint8_t rotation; +}; + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/esp-epaper-module/include/esp_epaper_module.h b/Drivers/esp-epaper-module/include/esp_epaper_module.h new file mode 100644 index 000000000..fb3d5f3de --- /dev/null +++ b/Drivers/esp-epaper-module/include/esp_epaper_module.h @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +extern struct Module esp_epaper_module; + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/esp-epaper-module/source/esp_epaper.cpp b/Drivers/esp-epaper-module/source/esp_epaper.cpp new file mode 100644 index 000000000..8bff9eea6 --- /dev/null +++ b/Drivers/esp-epaper-module/source/esp_epaper.cpp @@ -0,0 +1,353 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include "esp_epaper_rotate.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +#include +#include + +constexpr auto* TAG = "esp_epaper"; +#define GET_CONFIG(device) (static_cast((device)->config)) + +/** Width/height overrides above this are rejected as nonsense. */ +constexpr uint16_t MAX_PANEL_DIMENSION = 2048; + +struct EspEpaperInternal { + /** Opaque esp_epaper device, owns the panel's pins and SPI device. */ + epd_handle_t epd; + epd_panel_info_t info; + /** Scratch buffer in native panel layout, for rotated frames (rotation != 0). */ + uint8_t* rotate_buffer; + /** Serializes panel/SPI access between draw_bitmap and power state changes. */ + SemaphoreHandle_t panel_mutex; + /** disp_on_off state; the panel is in deep sleep while false. */ + bool display_on; +}; + +static uint16_t esp_epaper_get_display_width(const EspEpaperInternal* internal, uint8_t rotation) { + return esp_epaper_rotation_swaps_axes(rotation) ? internal->info.height : internal->info.width; +} + +static uint16_t esp_epaper_get_display_height(const EspEpaperInternal* internal, uint8_t rotation) { + return esp_epaper_rotation_swaps_axes(rotation) ? internal->info.width : internal->info.height; +} + +static bool resolve_panel_type(const char* name, epd_panel_type_t* out_type) { + struct PanelMapping { + const char* name; + epd_panel_type_t type; + }; + static constexpr PanelMapping kPanelMappings[] = { + { "gdey0154d67", EPD_PANEL_GDEY0154D67 }, + { "gdep073e01", EPD_PANEL_GDEP073E01 }, + { "gdey037f51", EPD_PANEL_GDEY037F51 }, + { "gdey029t71h", EPD_PANEL_GDEY029T71H }, + { "ssd16xx-154", EPD_PANEL_SSD16XX_154 }, + { "ssd16xx-213", EPD_PANEL_SSD16XX_213 }, + { "ssd16xx-266", EPD_PANEL_SSD16XX_266 }, + { "ssd16xx-270", EPD_PANEL_SSD16XX_270 }, + { "ssd16xx-290", EPD_PANEL_SSD16XX_290 }, + { "ssd16xx-370", EPD_PANEL_SSD16XX_370 }, + { "ssd16xx-420", EPD_PANEL_SSD16XX_420 }, + }; + for (const auto& mapping : kPanelMappings) { + if (std::strcmp(name, mapping.name) == 0) { + *out_type = mapping.type; + return true; + } + } + return false; +} + +static error_t esp_epaper_reset(Device* device) { + auto* internal = static_cast(device_get_driver_data(device)); + xSemaphoreTake(internal->panel_mutex, portMAX_DELAY); + // epd_wake() toggles the reset pin and re-runs the full init sequence. + const esp_err_t ret = epd_wake(internal->epd); + // epd_wake() re-inits the panel, so it is awake (and drawable) again. + if (ret == ESP_OK) { + internal->display_on = true; + } + xSemaphoreGive(internal->panel_mutex); + return ret == ESP_OK ? ERROR_NONE : ERROR_RESOURCE; +} + +static error_t esp_epaper_init(Device* device) { + auto* internal = static_cast(device_get_driver_data(device)); + xSemaphoreTake(internal->panel_mutex, portMAX_DELAY); + const esp_err_t ret = epd_wake(internal->epd); + if (ret == ESP_OK) { + internal->display_on = true; + } + xSemaphoreGive(internal->panel_mutex); + return ret == ESP_OK ? ERROR_NONE : ERROR_RESOURCE; +} + +// LVGL only ever calls this with the full frame: DISPLAY_COLOR_FORMAT_MONOCHROME forces +// LV_DISPLAY_RENDER_MODE_FULL in the generic kernel LVGL bridge (lvgl_display.c), and FULL mode +// only presents (calls draw_bitmap) once per render cycle, with the complete 0,0..hres,vres rect. +// hres/vres are the rotated (display) resolution reported by get_resolution_*; color_data is +// row-major, MSB-first 1bpp (LVGL's LV_COLOR_FORMAT_I1 with the palette header already stripped by +// the caller); bit 1 = white, bit 0 = black. epd_update() expects exactly that layout and polarity. +static error_t esp_epaper_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)); + const auto* config = GET_CONFIG(device); + + const uint16_t display_width = esp_epaper_get_display_width(internal, config->rotation); + const uint16_t display_height = esp_epaper_get_display_height(internal, config->rotation); + if (x_start != 0 || y_start != 0 || x_end != display_width || y_end != display_height) { + LOG_W(TAG, "draw_bitmap: only full-frame draws are supported (got %ld,%ld..%ld,%ld)", (long)x_start, (long)y_start, (long)x_end, (long)y_end); + return ERROR_NOT_SUPPORTED; + } + + xSemaphoreTake(internal->panel_mutex, portMAX_DELAY); + + if (!internal->display_on) { + // Display is off (deep sleep). Drop the frame; the mismatch is fine because the next + // power-on triggers a full refresh of the current render anyway. + xSemaphoreGive(internal->panel_mutex); + return ERROR_NONE; + } + + const uint8_t* source; + if (config->rotation == 0) { + source = static_cast(color_data); + } else { + memset(internal->rotate_buffer, 0, internal->info.buffer_size); + esp_epaper_rotate_frame(static_cast(color_data), internal->rotate_buffer, + internal->info.width, internal->info.height, config->rotation); + source = internal->rotate_buffer; + } + + const esp_err_t ret = epd_update(internal->epd, source, config->update_mode); + xSemaphoreGive(internal->panel_mutex); + if (ret != ESP_OK) { + LOG_E(TAG, "epd_update failed: %s", esp_err_to_name(ret)); + return ERROR_RESOURCE; + } + return ERROR_NONE; +} + +static error_t esp_epaper_disp_on_off(Device* device, bool on_off) { + auto* internal = static_cast(device_get_driver_data(device)); + + xSemaphoreTake(internal->panel_mutex, portMAX_DELAY); + if (on_off == internal->display_on) { + xSemaphoreGive(internal->panel_mutex); + return ERROR_NONE; + } + + bool ok = true; + if (on_off) { + if (epd_wake(internal->epd) != ESP_OK) { + LOG_E(TAG, "epd_wake failed"); + ok = false; + } + } else { + if (epd_sleep(internal->epd) != ESP_OK) { + LOG_E(TAG, "epd_sleep failed"); + ok = false; + } + } + + if (ok) { + internal->display_on = on_off; + } + xSemaphoreGive(internal->panel_mutex); + return ok ? ERROR_NONE : ERROR_RESOURCE; +} + +static DisplayColorFormat esp_epaper_get_color_format(Device*) { + return DISPLAY_COLOR_FORMAT_MONOCHROME; +} + +static uint16_t esp_epaper_get_resolution_x(Device* device) { + auto* internal = static_cast(device_get_driver_data(device)); + return esp_epaper_get_display_width(internal, GET_CONFIG(device)->rotation); +} + +static uint16_t esp_epaper_get_resolution_y(Device* device) { + auto* internal = static_cast(device_get_driver_data(device)); + return esp_epaper_get_display_height(internal, GET_CONFIG(device)->rotation); +} + +static void esp_epaper_get_frame_buffer(Device*, uint8_t, void** out_buffer) { + *out_buffer = nullptr; +} + +static uint8_t esp_epaper_get_frame_buffer_count(Device*) { + return 0; +} + +static const DisplayApi esp_epaper_display_api = { + .capabilities = DISPLAY_CAPABILITY_ON_OFF | DISPLAY_CAPABILITY_SLOW_REFRESH, + .reset = esp_epaper_reset, + .init = esp_epaper_init, + .draw_bitmap = esp_epaper_draw_bitmap, + .mirror = nullptr, + .swap_xy = nullptr, + .get_swap_xy = nullptr, + .get_mirror_x = nullptr, + .get_mirror_y = nullptr, + .set_gap = nullptr, + .get_gap_x = nullptr, + .get_gap_y = nullptr, + .invert_color = nullptr, + .disp_on_off = esp_epaper_disp_on_off, + .disp_sleep = nullptr, + .get_color_format = esp_epaper_get_color_format, + .get_resolution_x = esp_epaper_get_resolution_x, + .get_resolution_y = esp_epaper_get_resolution_y, + .get_frame_buffer = esp_epaper_get_frame_buffer, + .get_frame_buffer_count = esp_epaper_get_frame_buffer_count, + .get_backlight = nullptr, + .has_capability = nullptr, +}; + +static void free_internal(EspEpaperInternal* internal) { + if (internal->epd != nullptr) { + epd_deinit(internal->epd); + } + if (internal->rotate_buffer != nullptr) { + free(internal->rotate_buffer); + } + if (internal->panel_mutex != nullptr) { + vSemaphoreDelete(internal->panel_mutex); + } + free(internal); +} + +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); + + epd_panel_type_t panel_type; + if (!resolve_panel_type(config->panel_type, &panel_type)) { + LOG_E(TAG, "Unknown panel type: %s", config->panel_type); + return ERROR_NOT_SUPPORTED; + } + + if (config->clock_speed_hz == 0) { + LOG_E(TAG, "Invalid clock_speed_hz (0)"); + return ERROR_NOT_SUPPORTED; + } + + if (config->update_mode > EPD_UPDATE_PARTIAL) { + LOG_E(TAG, "Invalid update_mode %d (expected %d-%d)", (int)config->update_mode, (int)EPD_UPDATE_FULL, (int)EPD_UPDATE_PARTIAL); + return ERROR_NOT_SUPPORTED; + } + + if (config->width > MAX_PANEL_DIMENSION || config->height > MAX_PANEL_DIMENSION) { + LOG_E(TAG, "Invalid panel size %ux%u (maximum %ux%u)", (unsigned)config->width, (unsigned)config->height, (unsigned)MAX_PANEL_DIMENSION, (unsigned)MAX_PANEL_DIMENSION); + return ERROR_NOT_SUPPORTED; + } + + epd_config_t epd_config = EPD_CONFIG_DEFAULT(); + epd_config.pins.busy = config->pin_busy.pin; + epd_config.pins.rst = config->pin_reset.pin; + epd_config.pins.dc = config->pin_dc.pin; + epd_config.pins.cs = config->pin_cs.pin; + epd_config.pins.sck = spi_config->pin_sclk.pin; + epd_config.pins.mosi = spi_config->pin_mosi.pin; + epd_config.spi.host = spi_config->host; + epd_config.spi.speed_hz = config->clock_speed_hz; + epd_config.panel.type = panel_type; + epd_config.panel.width = config->width; + epd_config.panel.height = config->height; + + epd_handle_t epd = nullptr; + if (epd_init(&epd_config, &epd) != ESP_OK) { + LOG_E(TAG, "epd_init failed"); + return ERROR_RESOURCE; + } + + auto* internal = static_cast(calloc(1, sizeof(EspEpaperInternal))); + if (internal == nullptr) { + epd_deinit(epd); + return ERROR_OUT_OF_MEMORY; + } + + internal->epd = epd; + internal->panel_mutex = xSemaphoreCreateMutex(); + if (internal->panel_mutex == nullptr) { + free_internal(internal); + return ERROR_OUT_OF_MEMORY; + } + + if (epd_get_info(epd, &internal->info) != ESP_OK) { + LOG_E(TAG, "epd_get_info failed"); + free_internal(internal); + return ERROR_RESOURCE; + } + + if (internal->info.color_mode != EPD_COLOR_BW) { + LOG_E(TAG, "Panel color mode %d is not supported by the kernel display bridge (monochrome only)", internal->info.color_mode); + free_internal(internal); + return ERROR_NOT_SUPPORTED; + } + + if (config->rotation > 3) { + LOG_E(TAG, "Invalid rotation %u (expected 0-3)", config->rotation); + free_internal(internal); + return ERROR_NOT_SUPPORTED; + } + + if (config->rotation != 0) { + internal->rotate_buffer = static_cast(malloc(internal->info.buffer_size)); + if (internal->rotate_buffer == nullptr) { + LOG_E(TAG, "Failed to allocate %lu-byte rotation buffer", internal->info.buffer_size); + free_internal(internal); + return ERROR_OUT_OF_MEMORY; + } + } + + internal->display_on = true; + device_set_driver_data(device, internal); + + LOG_I(TAG, "Started %ux%u panel (buffer %lu bytes, rotation %u)", internal->info.width, internal->info.height, internal->info.buffer_size, config->rotation); + return ERROR_NONE; +} + +static error_t stop(Device* device) { + auto* internal = static_cast(device_get_driver_data(device)); + + xSemaphoreTake(internal->panel_mutex, portMAX_DELAY); + if (internal->display_on) { + // Leave the panel in deep sleep. + epd_sleep(internal->epd); + internal->display_on = false; + } + xSemaphoreGive(internal->panel_mutex); + + free_internal(internal); + device_set_driver_data(device, nullptr); + return ERROR_NONE; +} + +Driver esp_epaper_driver = { + .name = "esp_epaper", + .compatible = (const char*[]) { "tuanpmt,esp-epaper", nullptr }, + .start_device = start, + .stop_device = stop, + .api = &esp_epaper_display_api, + .device_type = &DISPLAY_TYPE, + .owner = &esp_epaper_module, + .internal = nullptr +}; diff --git a/Drivers/esp-epaper-module/source/esp_epaper_rotate.h b/Drivers/esp-epaper-module/source/esp_epaper_rotate.h new file mode 100644 index 000000000..1cd6ae48f --- /dev/null +++ b/Drivers/esp-epaper-module/source/esp_epaper_rotate.h @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Rotates a full 1bpp frame (row-major, MSB-first) from the display's rotated + * space into the panel's native space. The display is the panel's native + * width x height turned counter-clockwise by `rotation` (0 = 0, 1 = 90, 2 = 180, + * 3 = 270 degrees), so its own dimensions are the swapped native ones for + * 90/270. Each mapping is the inverse of LVGL's lv_display_rotate_area() + * (Libraries/lvgl/src/display/lv_display.c), i.e. the exact transform LVGL + * applies when displaying on the panel. + * + * src points to the display frame (stride = ceil(display_width / 8) bytes, + * MSB-first bits), dst to the native frame (stride = ceil(width / 8) bytes). + * dst is overwritten; it may not alias src. + */ +static inline bool esp_epaper_rotation_swaps_axes(uint8_t rotation) { + return rotation == 1 || rotation == 3; +} + +static inline void esp_epaper_rotate_frame(const uint8_t* src, uint8_t* dst, uint16_t width, uint16_t height, uint8_t rotation) { + const uint16_t display_width = esp_epaper_rotation_swaps_axes(rotation) ? height : width; + const uint32_t src_stride = (display_width + 7) / 8; + const uint32_t dst_stride = (width + 7) / 8; + + memset(dst, 0, dst_stride * height); + + for (uint16_t y = 0; y < height; y++) { + for (uint16_t x = 0; x < width; x++) { + // Display pixel (u, v) that lands on native pixel (x, y). + uint16_t u; + uint16_t v; + switch (rotation) { + case 0: + u = x; + v = y; + break; + case 1: + u = height - 1 - y; + v = x; + break; + case 2: + u = width - 1 - x; + v = height - 1 - y; + break; + case 3: + default: + u = y; + v = width - 1 - x; + break; + } + if (src[(uint32_t)v * src_stride + u / 8] & (0x80 >> (u % 8))) { + dst[(uint32_t)y * dst_stride + x / 8] |= (0x80 >> (x % 8)); + } + } + } +} + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/esp-epaper-module/source/module.cpp b/Drivers/esp-epaper-module/source/module.cpp new file mode 100644 index 000000000..9a24dde3f --- /dev/null +++ b/Drivers/esp-epaper-module/source/module.cpp @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include + +extern Driver esp_epaper_driver; + +extern "C" { + +static Driver* const esp_epaper_drivers[] = { + &esp_epaper_driver, + nullptr +}; + +Module esp_epaper_module = { + .name = "esp_epaper", + .drivers = esp_epaper_drivers +}; + +} // extern "C" diff --git a/Libraries/esp_epaper b/Libraries/esp_epaper new file mode 160000 index 000000000..0bf4f144b --- /dev/null +++ b/Libraries/esp_epaper @@ -0,0 +1 @@ +Subproject commit 0bf4f144b543048f10bdadd49f0578858bdc2a39 diff --git a/Tactility/Source/lvgl/UsbHidInput.cpp b/Tactility/Source/lvgl/UsbHidInput.cpp index 731046a19..87f3f8449 100644 --- a/Tactility/Source/lvgl/UsbHidInput.cpp +++ b/Tactility/Source/lvgl/UsbHidInput.cpp @@ -155,16 +155,24 @@ static void usbHidInputTask(void* arg) { lvgl_lock(); - ctx->mouse_cursor = lv_image_create(lv_layer_sys()); - lv_obj_remove_flag(ctx->mouse_cursor, LV_OBJ_FLAG_CLICKABLE); - lv_image_set_src(ctx->mouse_cursor, TT_ASSETS_UI_CURSOR); - lv_obj_add_flag(ctx->mouse_cursor, LV_OBJ_FLAG_HIDDEN); + // Without a registered display, lv_layer_sys() is NULL: creating the cursor image on it trips + // an LVGL assert whose default handler is an infinite loop (while(1);), hanging this task while + // it holds the LVGL lock. Only create the cursor when a system layer actually exists. + lv_obj_t* sys_layer = lv_layer_sys(); + if (sys_layer != nullptr) { + ctx->mouse_cursor = lv_image_create(sys_layer); + lv_obj_remove_flag(ctx->mouse_cursor, LV_OBJ_FLAG_CLICKABLE); + lv_image_set_src(ctx->mouse_cursor, TT_ASSETS_UI_CURSOR); + lv_obj_add_flag(ctx->mouse_cursor, LV_OBJ_FLAG_HIDDEN); + } ctx->mouse_indev = lv_indev_create(); lv_indev_set_type(ctx->mouse_indev, LV_INDEV_TYPE_POINTER); lv_indev_set_read_cb(ctx->mouse_indev, mouse_read_cb); lv_indev_set_user_data(ctx->mouse_indev, ctx); - lv_indev_set_cursor(ctx->mouse_indev, ctx->mouse_cursor); + if (ctx->mouse_cursor != nullptr) { + lv_indev_set_cursor(ctx->mouse_indev, ctx->mouse_cursor); + } ctx->kb_indev = lv_indev_create(); lv_indev_set_type(ctx->kb_indev, LV_INDEV_TYPE_KEYPAD); diff --git a/device.py b/device.py index f97a51999..448908258 100644 --- a/device.py +++ b/device.py @@ -254,6 +254,11 @@ def write_lvgl_variables(output_file, device_properties: dict): output_file.write("CONFIG_LV_THEME_DEFAULT_LIGHT=y\n") elif theme == "Mono": output_file.write("CONFIG_LV_USE_THEME_MONO=y\n") + # LVGL selects the theme with #if LV_USE_THEME_DEFAULT / #elif LV_USE_THEME_SIMPLE / + # #elif LV_USE_THEME_MONO, and the Kconfig defaults enable DEFAULT+SIMPLE for any + # non-1bpp color depth so MONO only takes effect once those are disabled. + output_file.write("CONFIG_LV_USE_THEME_DEFAULT=n\n") + output_file.write("CONFIG_LV_USE_THEME_SIMPLE=n\n") else: exit_with_error(f"Unknown theme: {theme}") font_height_text = get_property_or_default(device_properties, "lvgl.fontSize", "14") diff --git a/partitions-16mb-no-sd-dev.csv b/partitions-16mb-no-sd-dev.csv index 497ffa1ef..5d69f3878 100644 --- a/partitions-16mb-no-sd-dev.csv +++ b/partitions-16mb-no-sd-dev.csv @@ -3,5 +3,5 @@ nvs, data, nvs, 0x9000, 0x6000, phy_init, data, phy, 0xf000, 0x1000, factory, app, factory, 0x10000, 4M, -system, data, fat, , 100k, +system, data, fat, , 128k, data, data, fat, , 1000k, diff --git a/partitions-16mb-no-sd.csv b/partitions-16mb-no-sd.csv index daeee992d..65eeb5a8a 100644 --- a/partitions-16mb-no-sd.csv +++ b/partitions-16mb-no-sd.csv @@ -3,5 +3,5 @@ nvs, data, nvs, 0x9000, 0x6000, phy_init, data, phy, 0xf000, 0x1000, factory, app, factory, 0x10000, 4M, -system, data, fat, , 100k, +system, data, fat, , 128k, data, data, fat, , 11800k, diff --git a/partitions-16mb-with-sd.csv b/partitions-16mb-with-sd.csv index 262186df8..6e011704c 100644 --- a/partitions-16mb-with-sd.csv +++ b/partitions-16mb-with-sd.csv @@ -3,4 +3,4 @@ nvs, data, nvs, 0x9000, 0x6000, phy_init, data, phy, 0xf000, 0x1000, factory, app, factory, 0x10000, 4M, -system, data, fat, , 100k, +system, data, fat, , 128k, diff --git a/partitions-4mb-with-sd.csv b/partitions-4mb-with-sd.csv index 59d1243b4..bf83015c4 100644 --- a/partitions-4mb-with-sd.csv +++ b/partitions-4mb-with-sd.csv @@ -3,4 +3,4 @@ nvs, data, nvs, 0x9000, 0x6000, phy_init, data, phy, 0xf000, 0x1000, factory, app, factory, 0x10000, 3800k, -system, data, fat, , 100k, +system, data, fat, , 128k, diff --git a/partitions-8mb-no-sd-dev.csv b/partitions-8mb-no-sd-dev.csv index 497ffa1ef..5d69f3878 100644 --- a/partitions-8mb-no-sd-dev.csv +++ b/partitions-8mb-no-sd-dev.csv @@ -3,5 +3,5 @@ nvs, data, nvs, 0x9000, 0x6000, phy_init, data, phy, 0xf000, 0x1000, factory, app, factory, 0x10000, 4M, -system, data, fat, , 100k, +system, data, fat, , 128k, data, data, fat, , 1000k, diff --git a/partitions-8mb-no-sd.csv b/partitions-8mb-no-sd.csv index 79544b13d..bdc40599e 100644 --- a/partitions-8mb-no-sd.csv +++ b/partitions-8mb-no-sd.csv @@ -3,5 +3,5 @@ nvs, data, nvs, 0x9000, 0x6000, phy_init, data, phy, 0xf000, 0x1000, factory, app, factory, 0x10000, 4M, -system, data, fat, , 100k, +system, data, fat, , 128k, data, data, fat, , 3800k, diff --git a/partitions-8mb-with-sd.csv b/partitions-8mb-with-sd.csv index 262186df8..6e011704c 100644 --- a/partitions-8mb-with-sd.csv +++ b/partitions-8mb-with-sd.csv @@ -3,4 +3,4 @@ nvs, data, nvs, 0x9000, 0x6000, phy_init, data, phy, 0xf000, 0x1000, factory, app, factory, 0x10000, 4M, -system, data, fat, , 100k, +system, data, fat, , 128k, From 564d8af64c7f4254d4e96993141dbbf8f9424d98 Mon Sep 17 00:00:00 2001 From: NellowTCS Date: Sat, 8 Aug 2026 06:52:34 -0600 Subject: [PATCH 2/3] Implement Tulip Creative Computer 4r11 (#608) --- Devices/tulip-4r11/CMakeLists.txt | 7 + Devices/tulip-4r11/LICENSE-Apache-2.0.md | 195 ++++++++++++++++++ Devices/tulip-4r11/device.properties | 27 +++ Devices/tulip-4r11/devicetree.yaml | 7 + Devices/tulip-4r11/source/module.cpp | 9 + Devices/tulip-4r11/tulip-4r11.dts | 195 ++++++++++++++++++ .../bindings/ti,pcm5101a.yaml | 9 + .../include/bindings/dummy_i2s_amp.h | 1 + .../source/dummy_i2s_amp.cpp | 2 +- .../gt911-module/bindings/goodix,gt911.yaml | 33 ++- Drivers/gt911-module/include/drivers/gt911.h | 5 + Drivers/gt911-module/source/gt911.cpp | 80 ++++++- .../bindings/espressif,esp32-rgb-display.yaml | 8 + .../include/drivers/rgb_display.h | 15 ++ .../include/drivers/software_pixel_mapper.h | 61 ++++++ .../rgb-display-module/source/rgb_display.cpp | 63 +++++- .../source/software_pixel_mapper.cpp | 42 ++++ Tactility/Source/Tactility.cpp | 34 +++ partitions-32mb-no-sd-dev.csv | 7 + partitions-32mb-no-sd.csv | 7 + 20 files changed, 798 insertions(+), 9 deletions(-) create mode 100644 Devices/tulip-4r11/CMakeLists.txt create mode 100644 Devices/tulip-4r11/LICENSE-Apache-2.0.md create mode 100644 Devices/tulip-4r11/device.properties create mode 100644 Devices/tulip-4r11/devicetree.yaml create mode 100644 Devices/tulip-4r11/source/module.cpp create mode 100644 Devices/tulip-4r11/tulip-4r11.dts create mode 100644 Drivers/dummy-i2s-amp-module/bindings/ti,pcm5101a.yaml create mode 100644 Drivers/rgb-display-module/include/drivers/software_pixel_mapper.h create mode 100644 Drivers/rgb-display-module/source/software_pixel_mapper.cpp create mode 100644 partitions-32mb-no-sd-dev.csv create mode 100644 partitions-32mb-no-sd.csv diff --git a/Devices/tulip-4r11/CMakeLists.txt b/Devices/tulip-4r11/CMakeLists.txt new file mode 100644 index 000000000..09a2fdee3 --- /dev/null +++ b/Devices/tulip-4r11/CMakeLists.txt @@ -0,0 +1,7 @@ +file(GLOB_RECURSE SOURCE_FILES source/*.c*) + +idf_component_register( + SRCS ${SOURCE_FILES} + INCLUDE_DIRS "source" + REQUIRES TactilityKernel +) diff --git a/Devices/tulip-4r11/LICENSE-Apache-2.0.md b/Devices/tulip-4r11/LICENSE-Apache-2.0.md new file mode 100644 index 000000000..f5f4b8b5e --- /dev/null +++ b/Devices/tulip-4r11/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/Devices/tulip-4r11/device.properties b/Devices/tulip-4r11/device.properties new file mode 100644 index 000000000..c0ece6b84 --- /dev/null +++ b/Devices/tulip-4r11/device.properties @@ -0,0 +1,27 @@ +general.vendor=Tulip +general.name=Tulip Creative Computer (4r11) + +apps.launcherAppId=Launcher + +hardware.target=ESP32S3 +hardware.flashSize=32MB +hardware.spiRam=true +hardware.spiRamMode=OCT +hardware.spiRamSpeed=120M +hardware.esptoolFlashFreq=120M +hardware.usbHostEnabled=true +hardware.bluetooth=true + +storage.userDataLocation=Internal + +touch.calibrationSupported=true +touch.calibrationRequired=false + +display.size=7" +display.shape=rectangle +display.dpi=117 + +lvgl.colorDepth=16 +lvgl.fontSize=20 + +sdkconfig.CONFIG_CODEC_DUMMY_SUPPORT=y diff --git a/Devices/tulip-4r11/devicetree.yaml b/Devices/tulip-4r11/devicetree.yaml new file mode 100644 index 000000000..465ddb55b --- /dev/null +++ b/Devices/tulip-4r11/devicetree.yaml @@ -0,0 +1,7 @@ +dependencies: + - Platforms/platform-esp32 + - Drivers/rgb-display-module + - Drivers/gt911-module + - Drivers/dummy-i2s-amp-module + - Drivers/audio-stream-module +dts: tulip-4r11.dts diff --git a/Devices/tulip-4r11/source/module.cpp b/Devices/tulip-4r11/source/module.cpp new file mode 100644 index 000000000..8e5beb223 --- /dev/null +++ b/Devices/tulip-4r11/source/module.cpp @@ -0,0 +1,9 @@ +#include + +extern "C" { + +Module tulip_4r11_module = { + .name = "tulip-4r11" +}; + +} diff --git a/Devices/tulip-4r11/tulip-4r11.dts b/Devices/tulip-4r11/tulip-4r11.dts new file mode 100644 index 000000000..fd206859a --- /dev/null +++ b/Devices/tulip-4r11/tulip-4r11.dts @@ -0,0 +1,195 @@ +/dts-v1/; + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +// Reference: https: //github.com/TulipCC/tulipcc/ + / { + compatible = "root"; + model = "Tulip 4 (R11)"; + + wifi0 { + compatible = "espressif,esp32-wifi-pinned"; + status = "disabled"; + }; + + ble0 { + compatible = "espressif,esp32-ble"; + status = "disabled"; + }; + + gpio0 { + compatible = "espressif,esp32-gpio"; + gpio-count = <49>; + }; + + // Battery voltage is sensed through the 470K/470K divider (R34/R38) into GPIO3 + // (ADC1_CH2), i.e. a straight 2: 1 divider; the RY3730 charger IC has no I2C + // interface to drive. Reference: tulipcc tulip/shared/py/tulip.py battery(). + adc0 { + compatible = "espressif,esp32-adc-oneshot"; + unit-id = ; + channels = ; + }; + + battery-sense { + compatible = "battery-sense"; + io-channel = <&adc0 0>; + reference-voltage-mv = <3300>; + multiplier = <2000>; + }; + + // The Grove connector shares this I2C bus with the touch controller + i2c0 { + compatible = "espressif,esp32-i2c"; + port = ; + clock-frequency = <100000>; + pin-sda = <&gpio0 17 GPIO_FLAG_PULL_UP>; + pin-scl = <&gpio0 18 GPIO_FLAG_PULL_UP>; + + touch0 { + compatible = "goodix,gt911"; + reg = <0x5D>; + x-max = <1024>; + y-max = <600>; + // R11 inverts the reset line (active-high) and needs two toggle cycles before it + // starts responding on I2C. + pin-reset = <&gpio0 48 GPIO_FLAG_ACTIVE_HIGH>; + reset-pulses = <2>; + // The controller's programmed Y range (~750) exceeds the panel's 600 rows and the + // sensor sits slightly offset from the panel; calibrate per the reference firmware + x-offset = <-2>; + y-offset = <-10>; + y-scale = <800>; + }; + }; + + // The unused panel data lines (G2 and B3 are unconnected on R11) are driven low exactly + // like the reference firmware does, so the panel never sees floating inputs on them. + panel_data_low_b4 { + compatible = "gpio-hog"; + pin = <&gpio0 38 GPIO_FLAG_NONE>; + mode = ; + }; + + panel_data_low_b5 { + compatible = "gpio-hog"; + pin = <&gpio0 46 GPIO_FLAG_NONE>; + mode = ; + }; + + panel_data_low_g3 { + compatible = "gpio-hog"; + pin = <&gpio0 6 GPIO_FLAG_NONE>; + mode = ; + }; + + panel_data_low_g4 { + compatible = "gpio-hog"; + pin = <&gpio0 7 GPIO_FLAG_NONE>; + mode = ; + }; + + panel_data_low_r3 { + compatible = "gpio-hog"; + pin = <&gpio0 45 GPIO_FLAG_NONE>; + mode = ; + }; + + panel_data_low_r4 { + compatible = "gpio-hog"; + pin = <&gpio0 13 GPIO_FLAG_NONE>; + mode = ; + }; + + // Backlight is active-low: the reference firmware drives duty 0 at max brightness. + display_backlight_pwm { + compatible = "espressif,esp32-pwm-ledc"; + pin = <&gpio0 47 GPIO_FLAG_NONE>; + period-ns = <3333333>; + duty-resolution = <13>; + ledc-timer = <1>; + ledc-channel = <1>; + inverted; + }; + + display_backlight { + compatible = "pwm-backlight"; + pwm = <&display_backlight_pwm>; + brightness-level-range = <0 255>; + brightness-default = <200>; + }; + + display0 { + compatible = "espressif,esp32-rgb-display"; + horizontal-resolution = <1024>; + vertical-resolution = <600>; + pixel-clock-hz = <28000000>; + hsync-pulse-width = <30>; + hsync-back-porch = <16>; + hsync-front-porch = <210>; + vsync-pulse-width = <13>; + vsync-back-porch = <10>; + vsync-front-porch = <22>; + data-width = <8>; + bits-per-pixel = <8>; + custom-pixel-format = ; + num-fbs = <1>; + 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 9 GPIO_FLAG_NONE>; // B6 + pin-data1 = <&gpio0 1 GPIO_FLAG_NONE>; // B7 + pin-data2 = <&gpio0 15 GPIO_FLAG_NONE>; // G5 + pin-data3 = <&gpio0 16 GPIO_FLAG_NONE>; // G6 + pin-data4 = <&gpio0 4 GPIO_FLAG_NONE>; // G7 + pin-data5 = <&gpio0 10 GPIO_FLAG_NONE>; // R5 + pin-data6 = <&gpio0 21 GPIO_FLAG_NONE>; // R6 + pin-data7 = <&gpio0 14 GPIO_FLAG_NONE>; // R7 + backlight = <&display_backlight>; + }; + + i2s0 { + compatible = "espressif,esp32-i2s"; + port = ; + pin-bclk = <&gpio0 8 GPIO_FLAG_NONE>; + pin-ws = <&gpio0 2 GPIO_FLAG_NONE>; + pin-data-out = <&gpio0 5 GPIO_FLAG_NONE>; + }; + + speaker0 { + compatible = "ti,pcm5101a"; + i2s = <&i2s0>; + }; + + usbhost0 { + compatible = "espressif,esp32-usbhost"; + + usbhosthid0 { + compatible = "espressif,esp32-usbhost-hid"; + }; + + usbhostmidi0 { + compatible = "espressif,esp32-usbhost-midi"; + }; + + usbhostmsc0 { + compatible = "espressif,esp32-usbhost-msc"; + }; + }; +}; diff --git a/Drivers/dummy-i2s-amp-module/bindings/ti,pcm5101a.yaml b/Drivers/dummy-i2s-amp-module/bindings/ti,pcm5101a.yaml new file mode 100644 index 000000000..4e2a26fc7 --- /dev/null +++ b/Drivers/dummy-i2s-amp-module/bindings/ti,pcm5101a.yaml @@ -0,0 +1,9 @@ +description: TI PCM5101A stereo DAC (I2S input, no register interface, no GPIO enable) + +compatible: "ti,pcm5101a" + +properties: + i2s: + type: phandle + required: true + description: "I2S controller device that carries audio data" diff --git a/Drivers/dummy-i2s-amp-module/include/bindings/dummy_i2s_amp.h b/Drivers/dummy-i2s-amp-module/include/bindings/dummy_i2s_amp.h index 86ececb40..bee2eb514 100644 --- a/Drivers/dummy-i2s-amp-module/include/bindings/dummy_i2s_amp.h +++ b/Drivers/dummy-i2s-amp-module/include/bindings/dummy_i2s_amp.h @@ -14,6 +14,7 @@ extern "C" { // typedef even though they all share the same underlying config layout. DEFINE_DEVICETREE(max98357a, struct DummyI2sAmpConfig) DEFINE_DEVICETREE(ns4168, struct DummyI2sAmpConfig) +DEFINE_DEVICETREE(pcm5101a, struct DummyI2sAmpConfig) #ifdef __cplusplus } diff --git a/Drivers/dummy-i2s-amp-module/source/dummy_i2s_amp.cpp b/Drivers/dummy-i2s-amp-module/source/dummy_i2s_amp.cpp index d370c4743..c049a3235 100644 --- a/Drivers/dummy-i2s-amp-module/source/dummy_i2s_amp.cpp +++ b/Drivers/dummy-i2s-amp-module/source/dummy_i2s_amp.cpp @@ -373,7 +373,7 @@ extern "C" { Driver dummy_i2s_amp_driver = { .name = "dummy_i2s_amp", - .compatible = (const char*[]) { "maxim,max98357a", "nsiway,ns4168", nullptr }, + .compatible = (const char*[]) { "maxim,max98357a", "nsiway,ns4168", "ti,pcm5101a", nullptr }, .start_device = start_device, .stop_device = stop_device, .api = &API, diff --git a/Drivers/gt911-module/bindings/goodix,gt911.yaml b/Drivers/gt911-module/bindings/goodix,gt911.yaml index 9ec8341d1..e5e8b58b7 100644 --- a/Drivers/gt911-module/bindings/goodix,gt911.yaml +++ b/Drivers/gt911-module/bindings/goodix,gt911.yaml @@ -30,8 +30,39 @@ properties: pin-reset: type: phandles default: GPIO_PIN_SPEC_NONE - description: Reset GPIO pin + description: | + Reset GPIO pin. Reset polarity is set with the GPIO descriptor's ACTIVE_HIGH or + ACTIVE_LOW flag (active-low is the GT911's native convention; boards that invert the + reset line, e.g. Tulip 4 R11, use ACTIVE_HIGH). pin-interrupt: type: phandles default: GPIO_PIN_SPEC_NONE description: Interrupt GPIO pin + reset-pulses: + type: int + default: 1 + description: | + Number of reset pulses applied before the controller is initialized. Some panels + (e.g. Tulip 4 R11) require two toggle cycles before they respond on I2C. + x-offset: + type: int + default: 0 + description: | + Signed X coordinate correction added to the raw controller output before scaling. + Aligns the sensor with the panel (e.g. -2 on Tulip 4 R11). + y-offset: + type: int + default: 0 + description: | + Signed Y coordinate correction added to the raw controller output before scaling + (e.g. -12 on Tulip 4 R11). + x-scale: + type: int + default: 1000 + description: | + X coordinate scale as per-mille (1000 = 1.0), applied after x-offset. + y-scale: + type: int + default: 1000 + description: | + Y coordinate scale as per-mille (1000 = 1.0), applied after y-offset. diff --git a/Drivers/gt911-module/include/drivers/gt911.h b/Drivers/gt911-module/include/drivers/gt911.h index 14b3e4d07..beb0ff784 100644 --- a/Drivers/gt911-module/include/drivers/gt911.h +++ b/Drivers/gt911-module/include/drivers/gt911.h @@ -22,6 +22,11 @@ struct Gt911Config { bool mirror_y; struct GpioPinSpec pin_reset; struct GpioPinSpec pin_interrupt; + uint8_t reset_pulses; + int16_t x_offset; + int16_t y_offset; + uint16_t x_scale; + uint16_t y_scale; }; #ifdef __cplusplus diff --git a/Drivers/gt911-module/source/gt911.cpp b/Drivers/gt911-module/source/gt911.cpp index b8aaed5d6..098ad0b60 100644 --- a/Drivers/gt911-module/source/gt911.cpp +++ b/Drivers/gt911-module/source/gt911.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -16,6 +17,8 @@ #include #include #include +#include +#include #include @@ -31,6 +34,42 @@ static gpio_num_t pin_or_nc(const GpioPinSpec& pin) { return pin.gpio_controller == nullptr ? GPIO_NUM_NC : static_cast(pin.pin); } +// Reset polarity is carried by the pin_reset descriptor's ACTIVE_HIGH/ACTIVE_LOW flag, so the +// pin is always pulsed with logical levels here. Some boards also require several toggle cycles +// before the controller starts responding on I2C (e.g. Tulip 4 R11). The esp_lcd_touch_gt911 +// driver only performs a single reset, so the pin is pulsed here first +static error_t reset_controller_pin(const GpioPinSpec& pin, uint8_t pulses) { + if (pulses == 0 || pin.gpio_controller == nullptr) { + return ERROR_NONE; + } + + auto* descriptor = gpio_descriptor_acquire(pin.gpio_controller, pin.pin, pin.flags | GPIO_FLAG_DIRECTION_OUTPUT, GPIO_OWNER_GPIO); + if (descriptor == nullptr) { + LOG_E(TAG, "Failed to acquire reset pin"); + return ERROR_RESOURCE; + } + + for (uint8_t i = 0; i < pulses; ++i) { + bool last_pulse = i == pulses - 1; + error_t error = gpio_descriptor_set_level(descriptor, true); + if (error == ERROR_NONE) { + vTaskDelay(pdMS_TO_TICKS(11)); + error = gpio_descriptor_set_level(descriptor, false); + } + if (error == ERROR_NONE) { + vTaskDelay(pdMS_TO_TICKS(last_pulse ? 1000 : 60)); + } + if (error != ERROR_NONE) { + LOG_E(TAG, "Failed to pulse reset pin"); + gpio_descriptor_release(descriptor); + return error; + } + } + + gpio_descriptor_release(descriptor); + return ERROR_NONE; +} + // region Driver lifecycle // GT911's I2C address depends on the controller's INT pin level at power-up (board-strapped, not @@ -62,6 +101,32 @@ static esp_err_t create_io_handle(Device* parent, esp_lcd_panel_io_handle_t* out return ESP_ERR_NOT_SUPPORTED; } +// Applies the per-device affine calibration (offset + per-mille scale) in the esp_lcd_touch +// framework's own coordinate hook, so downstream consumers always receive panel-space coords. +// Called before mirror/swap in esp_lcd_touch_get_coordinates(). +static void gt911_process_coordinates( + esp_lcd_touch_handle_t tp, + uint16_t* x, + uint16_t* y, + uint16_t* strength, + uint8_t* point_count, + uint8_t max_point_count +) { + (void)strength; + (void)max_point_count; + auto* config = static_cast(tp->config.user_data); + if (config == nullptr) { + return; + } + + for (uint8_t i = 0; i < *point_count; i++) { + int32_t mapped_x = (static_cast(x[i]) + config->x_offset) * config->x_scale / 1000; + int32_t mapped_y = (static_cast(y[i]) + config->y_offset) * config->y_scale / 1000; + x[i] = static_cast(mapped_x < 0 ? 0 : (mapped_x > config->x_max ? config->x_max : mapped_x)); + y[i] = static_cast(mapped_y < 0 ? 0 : (mapped_y > config->y_max ? config->y_max : mapped_y)); + } +} + static error_t start(Device* device) { auto* parent = device_get_parent(device); check(device_get_type(parent) == &I2C_CONTROLLER_TYPE); @@ -73,6 +138,12 @@ static error_t start(Device* device) { return ERROR_OUT_OF_MEMORY; } + error_t error = reset_controller_pin(config->pin_reset, config->reset_pulses); + if (error != ERROR_NONE) { + free(internal); + return error; + } + esp_err_t ret = create_io_handle(parent, &internal->io_handle); if (ret != ESP_OK) { free(internal); @@ -84,9 +155,10 @@ static error_t start(Device* device) { .y_max = config->y_max, .rst_gpio_num = pin_or_nc(config->pin_reset), .int_gpio_num = pin_or_nc(config->pin_interrupt), - // GT911's reset and interrupt lines are both fixed active-low in hardware. + // Reset polarity comes from the pin_reset descriptor's ACTIVE_HIGH/ACTIVE_LOW flag; the + // interrupt line is fixed active-low in hardware. .levels = { - .reset = 0u, + .reset = (config->pin_reset.flags & GPIO_FLAG_ACTIVE_LOW) != 0 ? 0u : 1u, .interrupt = 0u, }, .flags = { @@ -94,9 +166,9 @@ static error_t start(Device* device) { .mirror_x = config->mirror_x ? 1u : 0u, .mirror_y = config->mirror_y ? 1u : 0u, }, - .process_coordinates = nullptr, + .process_coordinates = gt911_process_coordinates, .interrupt_callback = nullptr, - .user_data = nullptr, + .user_data = (void*)config, .driver_data = nullptr, }; diff --git a/Drivers/rgb-display-module/bindings/espressif,esp32-rgb-display.yaml b/Drivers/rgb-display-module/bindings/espressif,esp32-rgb-display.yaml index f0d43a666..0350747b4 100644 --- a/Drivers/rgb-display-module/bindings/espressif,esp32-rgb-display.yaml +++ b/Drivers/rgb-display-module/bindings/espressif,esp32-rgb-display.yaml @@ -222,3 +222,11 @@ properties: type: phandle default: "NULL" description: Optional reference to this display's backlight device + custom-pixel-format: + type: int + default: 0 + description: | + Optional software pixel-format conversion applied to scan-out pixels, when the host renders + in a different depth than the panel (LVGL always renders RGB565). + 0 = no conversion (RGB_DISPLAY_PIXEL_FORMAT_DEFAULT), 1 = RGB332 + (RGB_DISPLAY_PIXEL_FORMAT_RGB332) for 8 data-line panels. diff --git a/Drivers/rgb-display-module/include/drivers/rgb_display.h b/Drivers/rgb-display-module/include/drivers/rgb_display.h index f6aff1f09..2c36923c6 100644 --- a/Drivers/rgb-display-module/include/drivers/rgb_display.h +++ b/Drivers/rgb-display-module/include/drivers/rgb_display.h @@ -11,6 +11,18 @@ extern "C" { #include #include +/** + * Optional software pixel-format conversion applied to scan-out pixels. The panel itself always + * runs at bits_per_pixel; when the host renders in a different depth (LVGL always renders RGB565 + * for this driver), the selected mapper converts each tile before it reaches esp_lcd. + */ +enum RgbDisplayPixelFormat { + /** No conversion: scan-out pixels must already match bits_per_pixel. */ + RGB_DISPLAY_PIXEL_FORMAT_DEFAULT = 0, + /** Convert RGB565 to packed 8-bit RGB332 (3R, 3G, 2B), for 8 data-line panels. */ + RGB_DISPLAY_PIXEL_FORMAT_RGB332 = 1, +}; + struct RgbDisplayConfig { uint16_t horizontal_resolution; uint16_t vertical_resolution; @@ -84,6 +96,9 @@ struct RgbDisplayConfig { // Optional reference to this display's backlight device, NULL if none. struct Device* backlight; + + // Optional software pixel-format conversion for scan-out (see enum RgbDisplayPixelFormat). + enum RgbDisplayPixelFormat pixel_format; }; #ifdef __cplusplus diff --git a/Drivers/rgb-display-module/include/drivers/software_pixel_mapper.h b/Drivers/rgb-display-module/include/drivers/software_pixel_mapper.h new file mode 100644 index 000000000..f021409a7 --- /dev/null +++ b/Drivers/rgb-display-module/include/drivers/software_pixel_mapper.h @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +/** + * Opaque per-instance state for a software pixel mapper, allocated by SoftwarePixelMapperCreateFn + * and released by SoftwarePixelMapperDestroyFn. The instance owns its conversion destination + * buffer: the driver passes this handle as map()'s dst argument. + */ +typedef void* SoftwarePixelMapperData; + +/** + * Allocates a mapper instance, including its destination buffer, sized for a width x height + * source buffer. + * @param width source buffer width in pixels + * @param height source buffer height in pixels + * @return a non-null instance handle, or NULL on allocation failure + */ +typedef SoftwarePixelMapperData (*SoftwarePixelMapperCreateFn)(uint16_t width, uint16_t height); + +/** + * Converts pixel_count pixels from the mapper's source format to its destination format. + * @param data instance handle from SoftwarePixelMapperCreateFn + * @param src RGB565 source pixels + * @param dst destination buffer, typically the instance's own buffer (i.e. the data handle) + * @param pixel_count number of pixels to convert + */ +typedef void (*SoftwarePixelMapperMapFn)(SoftwarePixelMapperData data, const uint16_t* src, uint8_t* dst, uint32_t pixel_count); + +/** + * Releases a mapper instance created by SoftwarePixelMapperCreateFn, including its destination + * buffer. + * @param data instance handle to release + */ +typedef void (*SoftwarePixelMapperDestroyFn)(SoftwarePixelMapperData data); + +/** + * A software pixel-format conversion, used to translate a driver's native source depth into a + * panel's scan-out depth when the two differ. Each mapper owns its own destination buffer, so it + * can size the allocation for its output format. + */ +struct SoftwarePixelMapper { + SoftwarePixelMapperCreateFn create; + SoftwarePixelMapperMapFn map; + SoftwarePixelMapperDestroyFn destroy; +}; + +/** + * RGB565 to packed 8-bit RGB332 (3 bits red, 3 bits green, 2 bits blue) mapper for panels wired + * with only 8 data lines. + */ +extern const struct SoftwarePixelMapper software_pixel_mapper_rgb332; + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/rgb-display-module/source/rgb_display.cpp b/Drivers/rgb-display-module/source/rgb_display.cpp index 798b7d87e..7748af43e 100644 --- a/Drivers/rgb-display-module/source/rgb_display.cpp +++ b/Drivers/rgb-display-module/source/rgb_display.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include @@ -40,6 +41,12 @@ struct RgbDisplayInternal { // 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 for why. SemaphoreHandle_t frame_complete_semaphore; + // Software pixel-format conversion active when custom-pixel-format != DEFAULT (see start()). + // LVGL always renders RGB565 for this driver, so draw_bitmap() converts each tile through the + // mapper before handing it to esp_lcd. The mapper owns its scratch buffer, held in + // pixel_mapper_data. Null when no conversion is active. + const struct SoftwarePixelMapper* pixel_mapper; + SoftwarePixelMapperData pixel_mapper_data; }; // esp_lcd_rgb_panel's draw_bitmap() has a zero-copy path when color_data is one of the panel's @@ -128,6 +135,8 @@ static error_t start(Device* device) { if (internal == nullptr) { return ERROR_OUT_OF_MEMORY; } + internal->pixel_mapper = nullptr; + internal->pixel_mapper_data = nullptr; error_t reset_error = perform_hardware_reset(config); if (reset_error != ERROR_NONE) { @@ -231,6 +240,28 @@ static error_t start(Device* device) { return error; } + if (config->pixel_format != RGB_DISPLAY_PIXEL_FORMAT_DEFAULT) { + switch (config->pixel_format) { + case RGB_DISPLAY_PIXEL_FORMAT_RGB332: + internal->pixel_mapper = &software_pixel_mapper_rgb332; + break; + default: + LOG_E(TAG, "Unsupported pixel format %d", (int)config->pixel_format); + free(internal); + return ERROR_INVALID_ARGUMENT; + } + + // The mapper sizes and allocates its own whole-frame destination buffer, which lands in + // PSRAM on boards that have it (see software_pixel_mapper.cpp). + internal->pixel_mapper_data = internal->pixel_mapper->create(config->horizontal_resolution, config->vertical_resolution); + if (internal->pixel_mapper_data == nullptr) { + LOG_E(TAG, "Failed to create pixel mapper"); + esp_lcd_panel_del(internal->panel_handle); + free(internal); + return ERROR_OUT_OF_MEMORY; + } + } + internal->frame_complete_semaphore = xSemaphoreCreateBinary(); if (internal->frame_complete_semaphore == nullptr) { esp_lcd_panel_del(internal->panel_handle); @@ -264,6 +295,9 @@ static error_t stop(Device* device) { } vSemaphoreDelete(internal->frame_complete_semaphore); + if (internal->pixel_mapper != nullptr) { + internal->pixel_mapper->destroy(internal->pixel_mapper_data); + } free(internal); device_set_driver_data(device, nullptr); return ERROR_NONE; @@ -299,15 +333,33 @@ static bool rgb_display_color_data_is_frame_buffer(const RgbDisplayInternal* int return false; } +// Converts a contiguous RGB565 region into the configured scan-out format, if any. The +// conversion is delegated to the pixel mapper selected in start(); the result is written into the +// mapper's scratch buffer so the zero-copy frame-buffer wait logic below can treat it like any +// other caller-owned buffer. static error_t rgb_display_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 = rgb_display_color_data_is_frame_buffer(internal, color_data); + const void* source_data = color_data; + if (internal->pixel_mapper != nullptr) { + uint32_t pixel_count = (uint32_t)(x_end - x_start) * (uint32_t)(y_end - y_start); + // The mapper's own destination buffer is its instance data, so it is passed as both the + // instance handle and the output buffer. + internal->pixel_mapper->map( + internal->pixel_mapper_data, + static_cast(color_data), + static_cast(internal->pixel_mapper_data), + pixel_count + ); + source_data = internal->pixel_mapper_data; + } + + bool wait_for_scanout = rgb_display_color_data_is_frame_buffer(internal, source_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) { + if (esp_lcd_panel_draw_bitmap(internal->panel_handle, x_start, y_start, x_end, y_end, source_data) != ESP_OK) { return ERROR_RESOURCE; } @@ -375,7 +427,12 @@ static void rgb_display_get_frame_buffer(Device* device, uint8_t index, void** o static uint8_t rgb_display_get_frame_buffer_count(Device* device) { auto* internal = static_cast(device_get_driver_data(device)); - return internal->frame_buffer_count; + // A converted-format panel's frame buffer runs at the native bits_per_pixel, which LVGL can't + // write directly (it always renders RGB565 for this driver). Exposing it would make + // lvgl_display.c bind LVGL straight onto it and corrupt the buffer; instead report 0 so LVGL + // renders into its own RGB565 buffers and flushes per-tile through the conversion in + // draw_bitmap(). + return GET_CONFIG(device)->pixel_format != RGB_DISPLAY_PIXEL_FORMAT_DEFAULT ? 0 : internal->frame_buffer_count; } static error_t rgb_display_get_backlight(Device* device, Device** backlight) { diff --git a/Drivers/rgb-display-module/source/software_pixel_mapper.cpp b/Drivers/rgb-display-module/source/software_pixel_mapper.cpp new file mode 100644 index 000000000..28bc949b6 --- /dev/null +++ b/Drivers/rgb-display-module/source/software_pixel_mapper.cpp @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#if SOC_LCD_RGB_SUPPORTED + +#include + +#include + +// RGB332 packs each pixel into one byte: 3 bits red, 3 bits green, 2 bits blue. The byte layout +// maps straight onto an 8-data-line panel's significant color inputs (R7..R5, G7..G5, B7..B6), +// so each RGB565 channel's top bits land on the corresponding MSB lines +static void rgb332_map(SoftwarePixelMapperData data, const uint16_t* src, uint8_t* dst, uint32_t pixel_count) { + (void)data; + for (uint32_t i = 0; i < pixel_count; i++) { + uint16_t px = src[i]; + dst[i] = (uint8_t)(((px >> 13) & 0x07) << 5) | (((px >> 8) & 0x07) << 2) | ((px >> 3) & 0x03); + } +} + +// The destination buffer must hold a whole frame (1 byte/pixel for RGB332). A 1024x600 panel +// needs ~600KB, which only fits in PSRAM on most boards, prefer SPIRAM and fall back to whatever +// internal RAM is available. The returned handle is this buffer, passed back as map()'s dst. +static SoftwarePixelMapperData rgb332_create(uint16_t width, uint16_t height) { + size_t buffer_size = (size_t)width * height; + void* buffer = heap_caps_malloc(buffer_size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); + if (buffer == nullptr) { + buffer = heap_caps_malloc(buffer_size, MALLOC_CAP_DEFAULT); + } + return buffer; +} + +static void rgb332_destroy(SoftwarePixelMapperData data) { + heap_caps_free(data); +} + +const struct SoftwarePixelMapper software_pixel_mapper_rgb332 = { + .create = rgb332_create, + .map = rgb332_map, + .destroy = rgb332_destroy, +}; + +#endif // SOC_LCD_RGB_SUPPORTED diff --git a/Tactility/Source/Tactility.cpp b/Tactility/Source/Tactility.cpp index 805269e52..aebebfba8 100644 --- a/Tactility/Source/Tactility.cpp +++ b/Tactility/Source/Tactility.cpp @@ -22,12 +22,15 @@ #include #include #include +#include #include #include #include #include +#include +#include #include #include @@ -360,6 +363,33 @@ static void stopAppFromToolbar(lv_event_t*) { app::stop(); } +#ifdef CONFIG_TT_TOUCH_CALIBRATION_SUPPORTED + +// Applies the calibration persisted by the touch calibration app to the live pointer indev. +// lvgl_devices_attach() runs before onLvglStarted(), so the default indev already exists here. +static void applySavedTouchCalibration() { + settings::touch::TouchCalibrationSettings settings = settings::touch::loadOrGetDefault(); + if (!settings.enabled || !settings::touch::isValid(settings)) { + return; + } + + LvglPointerCalibration calibration = { + .x_min = settings.xMin, + .x_max = settings.xMax, + .y_min = settings.yMin, + .y_max = settings.yMax, + }; + + lvgl_lock(); + auto* indev = lvgl_pointer_get_default(); + if (indev != nullptr) { + lvgl_pointer_set_calibration(indev, &calibration); + } + lvgl_unlock(); +} + +#endif // CONFIG_TT_TOUCH_CALIBRATION_SUPPORTED + static void onLvglStarted() { ToolbarConfig toolbar_config = { .nav_action_callback = stopAppFromToolbar }; lvgl_toolbar_configure(&toolbar_config); @@ -379,6 +409,10 @@ static void onLvglStarted() { lvgl::initTrackball(); +#ifdef CONFIG_TT_TOUCH_CALIBRATION_SUPPORTED + applySavedTouchCalibration(); +#endif + memory_print_stats(); } diff --git a/partitions-32mb-no-sd-dev.csv b/partitions-32mb-no-sd-dev.csv new file mode 100644 index 000000000..5d69f3878 --- /dev/null +++ b/partitions-32mb-no-sd-dev.csv @@ -0,0 +1,7 @@ +# Name, Type, SubType, Offset, Size, Flags +# Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap +nvs, data, nvs, 0x9000, 0x6000, +phy_init, data, phy, 0xf000, 0x1000, +factory, app, factory, 0x10000, 4M, +system, data, fat, , 128k, +data, data, fat, , 1000k, diff --git a/partitions-32mb-no-sd.csv b/partitions-32mb-no-sd.csv new file mode 100644 index 000000000..21f89df11 --- /dev/null +++ b/partitions-32mb-no-sd.csv @@ -0,0 +1,7 @@ +# Name, Type, SubType, Offset, Size, Flags +# Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap +nvs, data, nvs, 0x9000, 0x6000, +phy_init, data, phy, 0xf000, 0x1000, +factory, app, factory, 0x10000, 4M, +system, data, fat, , 128k, +data, data, fat, , 28480k, From dc3f6104b8191708a59a87ed3b1fc3284f07c1c0 Mon Sep 17 00:00:00 2001 From: Shadowtrance Date: Sat, 8 Aug 2026 23:02:49 +1000 Subject: [PATCH 3/3] Papers3 improvements (#611) - And Tab5 keyboard improvements - Usb host keyboard improvements - Symbols - Requires the device.py change from CL-32 PR but that only affects visuals and not build breaking --- .../source/drivers/papers3_display.cpp | 90 +++++++------------ .../Source/devices/tab5_keyboard.cpp | 83 ++++++++++++----- .../include/lvgl/devices/display.h | 7 ++ .../lvgl-module/source/devices/devices.cpp | 4 +- .../lvgl-module/source/devices/display.cpp | 24 +++-- .../source/drivers/usb/esp32_usbhost_hid.cpp | 22 ++++- TactilityC/Source/symbols/esp_http_client.cpp | 9 ++ TactilityC/Source/symbols/freertos.cpp | 8 ++ TactilityC/Source/tt_init.cpp | 44 +++++++++ .../include/tactility/drivers/display.h | 13 ++- .../include/tactility/drivers/keyboard.h | 19 ++++ .../include/tactility/drivers/usb_host_hid.h | 13 ++- TactilityKernel/source/drivers/keyboard.cpp | 6 ++ TactilityKernel/source/symbols.c | 1 + 14 files changed, 251 insertions(+), 92 deletions(-) diff --git a/Devices/m5stack-papers3/source/drivers/papers3_display.cpp b/Devices/m5stack-papers3/source/drivers/papers3_display.cpp index d127bafce..949c1d435 100644 --- a/Devices/m5stack-papers3/source/drivers/papers3_display.cpp +++ b/Devices/m5stack-papers3/source/drivers/papers3_display.cpp @@ -11,46 +11,27 @@ #include #include +#include + #include #include #define TAG "Papers3Display" #define GET_CONFIG(device) (static_cast((device)->config)) -// Maps each src byte (8px, MSB-first, bit=1 -> white/0x0F) to the 4 packed dst bytes -// (2px/byte, EPDiy MODE_PACKING_2PPB nibble order) it produces, replacing a per-pixel -// branch loop with a table lookup. -static uint32_t s_unpack_lut[256]; - -static void init_unpack_lut() { - for (uint32_t byte = 0; byte < 256; byte++) { - uint8_t dst[4]; - for (int32_t pair = 0; pair < 4; pair++) { - const uint8_t bit0 = (byte >> (7 - pair * 2)) & 0x01U; - const uint8_t bit1 = (byte >> (7 - pair * 2 - 1)) & 0x01U; - const uint8_t p0 = bit0 ? 0x0FU : 0x00U; - const uint8_t p1 = bit1 ? 0x0FU : 0x00U; - dst[pair] = static_cast((p1 << 4U) | p0); - } - memcpy(&s_unpack_lut[byte], dst, sizeof(dst)); - } -} - extern "C" { extern Module m5stack_papers3_module; -// epd_hl_init() sets an internal already_initialized flag and has no matching deinit, so the -// highlevel state (and the framebuffer it owns) must persist across stop()/start() cycles and be -// reused rather than recreated - ported from the old deprecated-HAL EpdiyDisplay's identical -// s_hlInitialized/s_hlState statics. +// epd_hl_init() has no matching deinit and sets an internal already_initialized flag, so the +// highlevel state must persist across stop()/start() cycles and be reused rather than recreated. static bool s_hl_initialized = false; static EpdiyHighlevelState s_hl_state = {}; struct Papers3DisplayInternal { EpdiyHighlevelState hl_state; uint8_t* framebuffer; - // Scratch buffer for the I1(1bpp)->EPDiy(4bpp packed, 2px/byte) conversion in draw_bitmap(). + // Scratch buffer for the grayscale8->EPDiy(4bpp packed, 2px/byte) conversion in draw_bitmap(). uint8_t* packed_buffer; bool powered; }; @@ -76,16 +57,19 @@ static error_t papers3_display_reset(Device* device) { } static error_t papers3_display_init(Device* device) { + const auto* config = GET_CONFIG(device); auto* internal = static_cast(device_get_driver_data(device)); power_on(internal); - epd_clear(); - epd_hl_set_all_white(&internal->hl_state); + // The bootloader/boot-logo splash draws via partial refreshes that never get a real quality + // pass, leaving a faint ghost. Run a full clear now, before LVGL's first flush ever reaches + // draw_bitmap(), so it never has to undo content LVGL already put on screen. + epd_fullclear(&internal->hl_state, config->temperature_celsius); return ERROR_NONE; } -// LVGL only ever calls this with the full frame: DISPLAY_COLOR_FORMAT_MONOCHROME forces -// LV_DISPLAY_RENDER_MODE_FULL in the generic kernel LVGL bridge (lvgl_display.c), and FULL mode -// only presents (calls draw_bitmap) once per render cycle, with the complete 0,0..hres,vres rect. +// Reports GRAYSCALE8 (not MONOCHROME) so LVGL uses partial/tile updates instead of forcing +// full-frame - the bridge hardcodes full-frame for MONOCHROME/I1 regardless of capability flags. +// So draw_bitmap is called once per changed tile, not necessarily the whole panel. static error_t papers3_display_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)); const auto* config = GET_CONFIG(device); @@ -93,32 +77,26 @@ static error_t papers3_display_draw_bitmap(Device* device, int32_t x_start, int3 const int32_t width = x_end - x_start; const int32_t height = y_end - y_start; - // color_data is DISPLAY_COLOR_FORMAT_MONOCHROME: row-major, MSB-first 1bpp (LVGL's LV_COLOR_FORMAT_I1 - // with the palette header already stripped by the caller). Bit 1 = white/lit (LVGL's I1 blend - // sets a bit when the source luminance is above its threshold), bit 0 = black. + // color_data is DISPLAY_COLOR_FORMAT_GRAYSCALE8: row-major, 1 byte/pixel luminance + // (0x00=black..0xFF=white, matching LVGL's L8). EPDiy wants 4bpp packed (2px/byte, 0x0=black, + // 0xF=white) - a plain >>4 truncation preserves all 16 real gray levels the panel supports + // (this panel is not B/W-only; see MODE_GC16/GL16 in papers3-display.yaml's draw-mode doc). const auto* src = static_cast(color_data); - const size_t src_stride = static_cast(width + 7) / 8; + const size_t src_stride = static_cast(width); const size_t packed_stride = static_cast(width + 1) / 2; for (int32_t row = 0; row < height; row++) { const uint8_t* src_row = src + static_cast(row) * src_stride; uint8_t* dst_row = internal->packed_buffer + static_cast(row) * packed_stride; int32_t col = 0; - // Bulk path: one LUT lookup + 4-byte copy per 8 source pixels. - for (; col + 8 <= width; col += 8) { - memcpy(dst_row + col / 2, &s_unpack_lut[src_row[col / 8]], 4); - } - // Tail: fewer than 8 pixels left (width not a multiple of 8). - for (; col < width; col += 2) { - const uint8_t bit0 = (src_row[col / 8] >> (7 - (col % 8))) & 0x01U; - const uint8_t p0 = bit0 ? 0x0FU : 0x00U; - uint8_t p1 = 0; - if (col + 1 < width) { - const uint8_t bit1 = (src_row[(col + 1) / 8] >> (7 - ((col + 1) % 8))) & 0x01U; - p1 = bit1 ? 0x0FU : 0x00U; - } + for (; col + 2 <= width; col += 2) { + const uint8_t p0 = src_row[col] >> 4U; + const uint8_t p1 = src_row[col + 1] >> 4U; dst_row[col / 2] = static_cast((p1 << 4U) | p0); } + if (col < width) { // odd width: last column has no pair, low nibble unused + dst_row[col / 2] = static_cast(src_row[col] >> 4U); + } } const EpdRect update_area = { @@ -152,7 +130,7 @@ static error_t papers3_display_disp_on_off(Device* device, bool on_off) { } static DisplayColorFormat papers3_display_get_color_format(Device*) { - return DISPLAY_COLOR_FORMAT_MONOCHROME; + return DISPLAY_COLOR_FORMAT_GRAYSCALE8; } // epd_width()/epd_height() are the panel's native, unrotated dimensions (display->width/height in @@ -172,7 +150,7 @@ static uint16_t papers3_display_get_resolution_y(Device*) { static void papers3_display_get_frame_buffer(Device*, uint8_t, void** out_buffer) { // Not exposed via the generic fb-direct path: EPDiy's framebuffer is its own 4bpp packed - // format, not the DISPLAY_COLOR_FORMAT_MONOCHROME (1bpp) this driver reports - see + // format, not the DISPLAY_COLOR_FORMAT_GRAYSCALE8 (1 byte/pixel) this driver reports - see // get_frame_buffer_count() and draw_bitmap()'s conversion. *out_buffer = nullptr; } @@ -184,7 +162,9 @@ static uint8_t papers3_display_get_frame_buffer_count(Device*) { // endregion static const DisplayApi papers3_display_api = { - .capabilities = DISPLAY_CAPABILITY_ON_OFF | DISPLAY_CAPABILITY_REQUIRES_FULL_FRAME | DISPLAY_CAPABILITY_SLOW_REFRESH, + // PREFER_EXTERNAL_RAM: draw_bitmap() converts into packed_buffer before touching hardware, + // never DMAs from LVGL's pointer directly - frees LVGL's draw buffers from forced internal RAM. + .capabilities = DISPLAY_CAPABILITY_ON_OFF | DISPLAY_CAPABILITY_SLOW_REFRESH | DISPLAY_CAPABILITY_PREFER_EXTERNAL_RAM, .reset = papers3_display_reset, .init = papers3_display_init, .draw_bitmap = papers3_display_draw_bitmap, @@ -213,12 +193,6 @@ static const DisplayApi papers3_display_api = { static error_t start(Device* device) { const auto* config = GET_CONFIG(device); - static bool s_lut_initialized = false; - if (!s_lut_initialized) { - init_unpack_lut(); - s_lut_initialized = true; - } - auto* internal = static_cast(malloc(sizeof(Papers3DisplayInternal))); if (internal == nullptr) { return ERROR_OUT_OF_MEMORY; @@ -245,8 +219,12 @@ static error_t start(Device* device) { internal->framebuffer = epd_hl_get_framebuffer(&internal->hl_state); // Sized for the rotated (LVGL-facing) resolution - see get_resolution_x()/y()'s comment. + // ~260KB for this panel - a plain malloc() would land in scarce internal RAM. This buffer is + // only ever read once per draw_bitmap() call by epd_draw_rotated_image() (into epdiy's own + // SPIRAM-backed framebuffers, see highlevel.c), so it has no internal-RAM/DMA requirement and + // belongs in PSRAM instead, matching epdiy's own front_fb/back_fb/difference_fb allocations. const size_t packed_buffer_size = static_cast((epd_rotated_display_width() + 1) / 2) * static_cast(epd_rotated_display_height()); - internal->packed_buffer = static_cast(malloc(packed_buffer_size)); + internal->packed_buffer = static_cast(heap_caps_malloc(packed_buffer_size, MALLOC_CAP_SPIRAM)); if (internal->packed_buffer == nullptr) { LOG_E(TAG, "Failed to allocate packed pixel buffer"); epd_deinit(); diff --git a/Devices/m5stack-tab5/Source/devices/tab5_keyboard.cpp b/Devices/m5stack-tab5/Source/devices/tab5_keyboard.cpp index eed327756..3de918af2 100644 --- a/Devices/m5stack-tab5/Source/devices/tab5_keyboard.cpp +++ b/Devices/m5stack-tab5/Source/devices/tab5_keyboard.cpp @@ -35,6 +35,12 @@ static constexpr uint32_t REPEAT_RATE_MS = 80; // REG_INT_STAT polling (when no IRQ pin) and software key-repeat ticking. static constexpr uint32_t POLL_INTERVAL_MS = 20; +// Upper bound on events consumed per drain_events() call. Since the loop re-reads REG_EVENT_NUM +// each iteration rather than counting down a latched value, this caps the damage if the device +// ever reports a non-zero count that never drains - without it, that would spin forever holding +// the I2C bus. The device's own queue is far smaller than this, so it never limits normal bursts. +static constexpr uint8_t MAX_EVENTS_PER_DRAIN = 32; + // --------------------------------------------------------------------------- // Register addresses // --------------------------------------------------------------------------- @@ -117,6 +123,12 @@ static constexpr HidMapping KEY_MATRIX_HID_SYM[70] = { // Covers all codes present in the Tab5 matrix tables above. LV_KEY_* are plain uint32_t // constants - matching KeyboardKeyData::key's driver-defined contract and the same convention // m5stack-module's cardputer_keyboard.cpp kernel driver already uses. +// +// `ctrl` only selects the LVGL focus-navigation aliases for the arrow keys. Ctrl chords on +// ordinary keys are NOT folded into the returned value - the C0 control codes a terminal wants +// (Ctrl+C = 0x03, Ctrl+K = 0x0B, ...) collide with the LVGL constants returned here (LV_KEY_END = 3, +// LV_KEY_PREV = 11, ...), so Ctrl is reported out-of-band via KeyboardKeyData::ctrl instead and +// consumers that want control codes derive them themselves. // --------------------------------------------------------------------------- static uint32_t tab5_translate_key(uint8_t keycode, uint8_t modifier, bool ctrl) { const bool shift = (modifier & 0x22U) != 0U; @@ -172,6 +184,15 @@ static uint32_t now_ms() { return static_cast(esp_timer_get_time() / 1000); } +// Queued key event. Modifier state is captured here at enqueue time rather than read back from +// Tab5KeyboardInternal at dequeue time, since the user can release Ctrl before read_key() drains +// the event - and software key-repeat replays this same struct, so a held chord keeps its modifiers. +struct Tab5KeyEvent { + uint32_t key; + bool ctrl; + bool alt; +}; + struct Tab5KeyboardInternal { QueueHandle_t queue; @@ -181,6 +202,7 @@ struct Tab5KeyboardInternal { bool aa_held; bool aa_tapped; bool ctrl_held; + bool alt_held; // IRQ-driven event gating volatile bool irq_pending; @@ -193,7 +215,7 @@ struct Tab5KeyboardInternal { uint32_t last_poll_ms; // Software key-repeat state (tracked by position to survive modifier changes) - uint32_t repeat_key; + Tab5KeyEvent repeat_event; uint8_t repeat_row; uint8_t repeat_col; uint32_t repeat_start_ms; @@ -289,16 +311,22 @@ static void remove_irq_pin(Tab5KeyboardInternal* internal) { // drain_events - reads all pending events from the device queue // --------------------------------------------------------------------------- static void drain_events(Device* device, Tab5KeyboardInternal* internal) { - uint8_t count = 0; - if (!read_reg(device, REG_EVENT_NUM, &count) || count == 0) { - return; - } + // REG_EVENT_NUM is re-read every iteration rather than latched once and counted down, matching + // M5's own UnitTab5Keyboard::drain_events(). Each REG_KEY_EVENT read consumes one event from the + // device queue, so a count latched up front can go stale mid-drain; re-reading makes the loop + // self-correcting and lets it stop as soon as the device says the queue is actually empty. + uint8_t drained = 0; + while (drained < MAX_EVENTS_PER_DRAIN) { + uint8_t count = 0; + if (!read_reg(device, REG_EVENT_NUM, &count) || count == 0) { + break; + } - while (count > 0) { uint8_t raw = 0; if (!read_reg(device, REG_KEY_EVENT, &raw) || raw == KEY_EVENT_EMPTY) { break; } + drained++; const bool pressed = (raw & 0x80U) != 0U; const uint8_t row = (raw >> 4U) & 0x07U; @@ -308,7 +336,6 @@ static void drain_events(Device* device, Tab5KeyboardInternal* internal) { if (row == MOD_ROW_SYM && col == MOD_COL_SYM) { internal->sym_active = pressed; update_leds(device, internal); - count--; continue; } if (row == MOD_ROW_AA && col == MOD_COL_AA) { @@ -324,16 +351,14 @@ static void drain_events(Device* device, Tab5KeyboardInternal* internal) { internal->aa_tapped = false; } update_leds(device, internal); - count--; continue; } if (row == MOD_ROW_CTRL && col == MOD_COL_CTRL) { internal->ctrl_held = pressed; - count--; continue; } if (row == MOD_ROW_ALT && col == MOD_COL_ALT) { - count--; + internal->alt_held = pressed; continue; } @@ -354,10 +379,11 @@ static void drain_events(Device* device, Tab5KeyboardInternal* internal) { // no business reaching into, so ESC is now just queued as a normal key // like everything else (LVGL/app code already handles ESC via focus/group // navigation the same way a dedicated ESC key on any other keyboard would). - xQueueSend(internal->queue, &lv_key, 0); + const Tab5KeyEvent event = { lv_key, internal->ctrl_held, internal->alt_held }; + xQueueSend(internal->queue, &event, 0); // Arm software repeat tracking by row/col to survive modifier changes const uint32_t now = now_ms(); - internal->repeat_key = lv_key; + internal->repeat_event = event; internal->repeat_row = row; internal->repeat_col = col; internal->repeat_start_ms = now; @@ -370,12 +396,11 @@ static void drain_events(Device* device, Tab5KeyboardInternal* internal) { } } else if (row == internal->repeat_row && col == internal->repeat_col) { // Match release by position, not translated value — survives sticky Aa clear - internal->repeat_key = 0; + internal->repeat_event.key = 0; } } } } - count--; } // Clear INT status after draining so the line de-asserts @@ -434,13 +459,19 @@ static void poll_if_due(Device* device, Tab5KeyboardInternal* internal) { drain_events(device, internal); } - // Software key-repeat (runs every tick regardless of IRQ) - if (internal->repeat_key != 0U) { - if ((now - internal->repeat_start_ms) >= REPEAT_INITIAL_MS) { + // Software key-repeat (runs every tick regardless of IRQ). + // + // The clock is re-read here rather than reusing `now` from the top of the function: a press + // handled by the drain above sets repeat_start_ms to a timestamp taken *during* the drain, which + // is later than `now`. The unsigned subtraction below would then wrap to a huge value and clear + // the REPEAT_INITIAL_MS gate immediately, emitting one spurious repeat ~1ms after every press. + const uint32_t repeat_now = now_ms(); + if (internal->repeat_event.key != 0U) { + if ((repeat_now - internal->repeat_start_ms) >= REPEAT_INITIAL_MS) { const uint32_t last = internal->repeat_last_ms; - if (last == 0 || (now - last) >= REPEAT_RATE_MS) { - internal->repeat_last_ms = now; - xQueueSend(internal->queue, &internal->repeat_key, 0); + if (last == 0 || (repeat_now - last) >= REPEAT_RATE_MS) { + internal->repeat_last_ms = repeat_now; + xQueueSend(internal->queue, &internal->repeat_event, 0); } } } @@ -464,7 +495,7 @@ static error_t start(Device* device) { } memset(internal, 0, sizeof(Tab5KeyboardInternal)); - internal->queue = xQueueCreate(20, sizeof(uint32_t)); + internal->queue = xQueueCreate(20, sizeof(Tab5KeyEvent)); if (internal->queue == nullptr) { free(internal); return ERROR_OUT_OF_MEMORY; @@ -522,15 +553,19 @@ static error_t tab5_keyboard_read_key(Device* device, KeyboardKeyData* data) { poll_if_due(device, internal); - uint32_t lv_key = 0; - if (xQueueReceive(internal->queue, &lv_key, 0) == pdTRUE) { - data->key = lv_key; + Tab5KeyEvent event = {}; + if (xQueueReceive(internal->queue, &event, 0) == pdTRUE) { + data->key = event.key; data->pressed = true; data->continue_reading = uxQueueMessagesWaiting(internal->queue) > 0; + data->ctrl = event.ctrl; + data->alt = event.alt; } else { data->key = 0; data->pressed = false; data->continue_reading = false; + data->ctrl = false; + data->alt = false; } return ERROR_NONE; diff --git a/Modules/lvgl-module/include/lvgl/devices/display.h b/Modules/lvgl-module/include/lvgl/devices/display.h index 2e7b26232..3dfcc3926 100644 --- a/Modules/lvgl-module/include/lvgl/devices/display.h +++ b/Modules/lvgl-module/include/lvgl/devices/display.h @@ -53,6 +53,13 @@ struct LvglDisplayConfig { * the LV_COLOR_FORMAT_I1 path (already always-full-frame). */ bool force_full_frame; + + /** + * Opts owned draw buffer(s) OUT of DMA-capable memory, falling back to PSRAM instead of + * scarce internal RAM. Default false keeps existing behavior. Only set true if the driver + * never DMAs directly from the buffer pointer LVGL hands it in the flush callback. + */ + bool prefer_external_ram; }; /** diff --git a/Modules/lvgl-module/source/devices/devices.cpp b/Modules/lvgl-module/source/devices/devices.cpp index 6b70b80ed..196cc26c7 100644 --- a/Modules/lvgl-module/source/devices/devices.cpp +++ b/Modules/lvgl-module/source/devices/devices.cpp @@ -67,11 +67,13 @@ void lvgl_devices_attach() { // itself is never asked to do something it can't. bool can_hw_rotate = display_has_capability(kernel_display_device, DISPLAY_CAPABILITY_CAP_SWAP_XY) && display_has_capability(kernel_display_device, DISPLAY_CAPABILITY_CAP_MIRROR); + bool prefer_external_ram_buffer = display_has_capability(kernel_display_device, DISPLAY_CAPABILITY_PREFER_EXTERNAL_RAM); struct LvglDisplayConfig lvgl_display_config = { .buffer_height = (uint16_t)(vres > 10 ? vres / 10 : vres), .sw_rotate = !can_hw_rotate, .swap_bytes = swap_bytes, - .force_full_frame = display_requires_full_frame + .force_full_frame = display_requires_full_frame, + .prefer_external_ram = prefer_external_ram_buffer }; lv_disp_t* added_display = NULL; if (lvgl_display_add(kernel_display_device, &lvgl_display_config, &added_display) == ERROR_NONE) { diff --git a/Modules/lvgl-module/source/devices/display.cpp b/Modules/lvgl-module/source/devices/display.cpp index b43a7927e..82d9eb529 100644 --- a/Modules/lvgl-module/source/devices/display.cpp +++ b/Modules/lvgl-module/source/devices/display.cpp @@ -59,13 +59,18 @@ struct LvglDisplayCtx { bool byte_swap; }; -static void* lvgl_display_alloc_buffer(size_t size_bytes) { +static void* lvgl_display_alloc_buffer(size_t size_bytes, bool prefer_external_ram) { #ifdef ESP_PLATFORM // Must match LV_DRAW_BUF_ALIGN (can be > 4 - e.g. 64, tied to the cache line size for // DMA2D/PPA coherency on some targets - see sdkconfig's CONFIG_LV_DRAW_BUF_ALIGN). A buffer // allocated less strictly than that fails lv_display_set_buffers()'s alignment assert, which // is configured to LV_ASSERT_HANDLER (while(1);) rather than a clean abort - i.e. a silent hang. - void* buf = heap_caps_aligned_alloc(LV_DRAW_BUF_ALIGN, size_bytes, MALLOC_CAP_DMA | MALLOC_CAP_8BIT); + // MALLOC_CAP_DMA is scarce internal RAM - skip it for displays that don't DMA directly from + // this buffer (see prefer_external_ram_buffer). Dropping MALLOC_CAP_DMA alone isn't enough to + // land in PSRAM though: MALLOC_CAP_8BIT alone is still satisfied by internal RAM, so + // MALLOC_CAP_SPIRAM must be requested explicitly (confirmed on real hardware). + uint32_t caps = prefer_external_ram ? (MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT) : (MALLOC_CAP_DMA | MALLOC_CAP_8BIT); + void* buf = heap_caps_aligned_alloc(LV_DRAW_BUF_ALIGN, size_bytes, caps); if (buf == NULL) { buf = heap_caps_aligned_alloc(LV_DRAW_BUF_ALIGN, size_bytes, MALLOC_CAP_DEFAULT); } @@ -104,6 +109,13 @@ static bool lvgl_display_map_color_format(enum DisplayColorFormat in, lv_color_f // (e.g. ssd1306_draw_bitmap()'s row-to-page transpose). *out = LV_COLOR_FORMAT_I1; return true; + case DISPLAY_COLOR_FORMAT_GRAYSCALE8: + // Row-major, 1 byte/pixel luminance (0x00=black, 0xFF=white) - matches LV_COLOR_FORMAT_L8 + // directly, no repacking needed. Deliberately NOT routed through the I1 branch below in + // lvgl_display_add(): I1 is hardcoded to LV_DISPLAY_RENDER_MODE_FULL there, which is what + // this format exists to avoid for panels that want real partial/tile updates. + *out = LV_COLOR_FORMAT_L8; + return true; default: return false; } @@ -396,7 +408,7 @@ error_t lvgl_display_add(struct Device* device, const struct LvglDisplayConfig* // 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); + ctx->buf1 = lvgl_display_alloc_buffer(buf_size_bytes, config->prefer_external_ram); if (ctx->buf1 == NULL) { delete wrapper; return ERROR_OUT_OF_MEMORY; @@ -416,13 +428,13 @@ error_t lvgl_display_add(struct Device* device, const struct LvglDisplayConfig* ? vres : config->buffer_height; buf_size_bytes = (size_t)hres * buf_height * bpp; - ctx->buf1 = lvgl_display_alloc_buffer(buf_size_bytes); + ctx->buf1 = lvgl_display_alloc_buffer(buf_size_bytes, config->prefer_external_ram); if (ctx->buf1 == NULL) { delete wrapper; return ERROR_OUT_OF_MEMORY; } if (config->double_buffer) { - ctx->buf2 = lvgl_display_alloc_buffer(buf_size_bytes); + ctx->buf2 = lvgl_display_alloc_buffer(buf_size_bytes, config->prefer_external_ram); if (ctx->buf2 == NULL) { lvgl_display_free_buffer(ctx->buf1); delete wrapper; @@ -436,7 +448,7 @@ error_t lvgl_display_add(struct Device* device, const struct LvglDisplayConfig* ctx->buf_size_bytes = buf_size_bytes; if (ctx->sw_rotate) { - ctx->rotate_buf = lvgl_display_alloc_buffer(buf_size_bytes); + ctx->rotate_buf = lvgl_display_alloc_buffer(buf_size_bytes, config->prefer_external_ram); if (ctx->rotate_buf == NULL) { if (ctx->owns_buffers) { lvgl_display_free_buffer(ctx->buf1); diff --git a/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost_hid.cpp b/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost_hid.cpp index dd20bb8e6..dd55b1f73 100644 --- a/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost_hid.cpp +++ b/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost_hid.cpp @@ -108,7 +108,16 @@ static uint32_t hid_keycode_to_key(uint8_t modifier, uint8_t key_code, default: break; } - if (ctrl || alt) return 0; + /* + * Ctrl and Alt no longer suppress the key. + * + * They used to return 0 here, which meant a chord like Ctrl+C produced nothing at all and a + * terminal application could never see it. The modifiers are now reported alongside the key in + * UsbHidEvent instead, so the plain character still comes through and a consumer that wants a + * control code derives it. Alt is passed through on the same basis. + */ + (void)ctrl; + (void)alt; if (key_code < (sizeof(keycode2ascii) / sizeof(keycode2ascii[0]))) { bool is_letter = (key_code >= 0x04 && key_code <= 0x1D); @@ -157,6 +166,10 @@ static void hid_interface_callback(hid_host_device_handle_t handle, if (params.proto == HID_PROTOCOL_KEYBOARD) { if (data_len < sizeof(hid_keyboard_input_report_boot_t)) break; auto* kb = reinterpret_cast(data); + const bool with_ctrl = (kb->modifier.val & HID_LEFT_CONTROL) || + (kb->modifier.val & HID_RIGHT_CONTROL); + const bool with_alt = (kb->modifier.val & HID_LEFT_ALT) || + (kb->modifier.val & HID_RIGHT_ALT); for (int i = 0; i < HID_KEYBOARD_KEY_MAX; i++) { uint8_t prev_hid = ctx->prev_keys[i]; @@ -169,7 +182,7 @@ static void hid_interface_callback(hid_host_device_handle_t handle, uint32_t lv_key = ctx->pressed_lv_keys[prev_hid]; ctx->pressed_lv_keys[prev_hid] = 0; if (lv_key) { - UsbHidEvent evt = { .type = USB_HID_EVENT_KEY, .key = { lv_key, false } }; + UsbHidEvent evt = { .type = USB_HID_EVENT_KEY, .key = { lv_key, false, with_ctrl, with_alt } }; publish_event(ctx, &evt); } } @@ -208,7 +221,10 @@ static void hid_interface_callback(hid_host_device_handle_t handle, uint32_t lv_key = hid_keycode_to_key(kb->modifier.val, hid_code, ctx->caps_lock_active, ctx->num_lock_active); if (lv_key) { - UsbHidEvent evt = { .type = USB_HID_EVENT_KEY, .key = { lv_key, true } }; + UsbHidEvent evt = { + .type = USB_HID_EVENT_KEY, + .key = { lv_key, true, with_ctrl, with_alt } + }; publish_event(ctx, &evt); ctx->pressed_lv_keys[hid_code] = lv_key; } diff --git a/TactilityC/Source/symbols/esp_http_client.cpp b/TactilityC/Source/symbols/esp_http_client.cpp index 99a6fe225..89b5791bf 100644 --- a/TactilityC/Source/symbols/esp_http_client.cpp +++ b/TactilityC/Source/symbols/esp_http_client.cpp @@ -3,9 +3,18 @@ #include +#include #include +#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE +#include +#endif const esp_elfsym esp_http_client_symbols[] = { +#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE + // Needed for HTTPS: an app passes this as crt_bundle_attach to validate certificates against + // the bundle already compiled into the firmware (CONFIG_MBEDTLS_CERTIFICATE_BUNDLE). + ESP_ELFSYM_EXPORT(esp_crt_bundle_attach), +#endif ESP_ELFSYM_EXPORT(esp_http_client_init), ESP_ELFSYM_EXPORT(esp_http_client_perform), ESP_ELFSYM_EXPORT(esp_http_client_cancel_request), diff --git a/TactilityC/Source/symbols/freertos.cpp b/TactilityC/Source/symbols/freertos.cpp index ae98521af..26389e1ae 100644 --- a/TactilityC/Source/symbols/freertos.cpp +++ b/TactilityC/Source/symbols/freertos.cpp @@ -1,3 +1,4 @@ +#include #include #include @@ -40,6 +41,7 @@ const esp_elfsym freertos_symbols[] = { ESP_ELFSYM_EXPORT(xTaskGenericNotify), ESP_ELFSYM_EXPORT(xTaskGenericNotifyFromISR), ESP_ELFSYM_EXPORT(ulTaskGenericNotifyTake), + ESP_ELFSYM_EXPORT(xTaskGetCurrentTaskHandle), ESP_ELFSYM_EXPORT(xTaskGetTickCount), ESP_ELFSYM_EXPORT(xTaskGetTickCountFromISR), ESP_ELFSYM_EXPORT(pvTaskGetThreadLocalStoragePointer), @@ -101,6 +103,12 @@ const esp_elfsym freertos_symbols[] = { ESP_ELFSYM_EXPORT(vPortYield), ESP_ELFSYM_EXPORT(vPortEnterCritical), ESP_ELFSYM_EXPORT(vPortExitCritical), +#if defined(CONFIG_IDF_TARGET_ESP32P4) || defined(CONFIG_IDF_TARGET_ESP32S3) + ESP_ELFSYM_EXPORT(xPortEnterCriticalTimeout), +#endif +#if defined(CONFIG_IDF_TARGET_ESP32P4) + ESP_ELFSYM_EXPORT(vPortExitCriticalMultiCore), +#endif ESP_ELFSYM_EXPORT(xPortInIsrContext), ESP_ELFSYM_EXPORT(xPortCanYield), ESP_ELFSYM_EXPORT(xPortGetCoreID), diff --git a/TactilityC/Source/tt_init.cpp b/TactilityC/Source/tt_init.cpp index 296b9f68b..fd5dd2954 100644 --- a/TactilityC/Source/tt_init.cpp +++ b/TactilityC/Source/tt_init.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #include #include #include @@ -89,6 +90,7 @@ const esp_elfsym main_symbols[] { ESP_ELFSYM_EXPORT(close), ESP_ELFSYM_EXPORT(rmdir), ESP_ELFSYM_EXPORT(unlink), + ESP_ELFSYM_EXPORT(open), // strings.h ESP_ELFSYM_EXPORT(explicit_bzero), ESP_ELFSYM_EXPORT(strcasecmp), @@ -194,6 +196,10 @@ const esp_elfsym main_symbols[] { ESP_ELFSYM_EXPORT(fgets), ESP_ELFSYM_EXPORT(fopen), ESP_ELFSYM_EXPORT(freopen), + // Lets an app find the descriptor behind a stream. Needed when stdin/stdout have been pointed + // somewhere other than descriptors 0 and 1, which is the case for an app that owns a terminal. + ESP_ELFSYM_EXPORT(fileno), + ESP_ELFSYM_EXPORT(setvbuf), ESP_ELFSYM_EXPORT(fputc), ESP_ELFSYM_EXPORT(fputs), ESP_ELFSYM_EXPORT(fprintf), @@ -239,6 +245,7 @@ const esp_elfsym main_symbols[] { ESP_ELFSYM_EXPORT(memchr), ESP_ELFSYM_EXPORT(memmove), ESP_ELFSYM_EXPORT(strdup), + ESP_ELFSYM_EXPORT(stpcpy), // ctype ESP_ELFSYM_EXPORT(isalnum), @@ -256,6 +263,13 @@ const esp_elfsym main_symbols[] { ESP_ELFSYM_EXPORT(toupper), // ESP-IDF ESP_ELFSYM_EXPORT(esp_log), + // Lets an app that has taken over the display quieten firmware logging: with stdout redirected + // to a terminal the app owns, anything logged appears on screen as if the app had printed it. + ESP_ELFSYM_EXPORT(esp_log_level_set), + // Lets an app redirect firmware logging somewhere other than stdout. An app that has pointed + // stdout at its own terminal needs this: without it, every ESP_LOG from any component in the + // system - NTP, RTC, TLS - paints over the app's display. + ESP_ELFSYM_EXPORT(esp_log_set_vprintf), ESP_ELFSYM_EXPORT(esp_log_write), ESP_ELFSYM_EXPORT(esp_log_timestamp), ESP_ELFSYM_EXPORT(esp_err_to_name), @@ -327,9 +341,26 @@ const esp_elfsym main_symbols[] { ESP_ELFSYM_EXPORT(ipaddr_addr), // POSIX socket names (VFS wrappers used when apps include ) ESP_ELFSYM_EXPORT(select), + // stdlib.h - environment and sorting + ESP_ELFSYM_EXPORT(getenv), + ESP_ELFSYM_EXPORT(setenv), + ESP_ELFSYM_EXPORT(unsetenv), + ESP_ELFSYM_EXPORT(qsort), + // unistd.h + ESP_ELFSYM_EXPORT(access), + ESP_ELFSYM_EXPORT(isatty), + ESP_ELFSYM_EXPORT(read), + ESP_ELFSYM_EXPORT(write), + ESP_ELFSYM_EXPORT(lseek), // sys/stat.h ESP_ELFSYM_EXPORT(stat), ESP_ELFSYM_EXPORT(mkdir), + // esp_vfs.h - lets an app register a device node (e.g. a terminal at /dev/...) so that plain + // printf/stdout reaches it. Note the registration is global and outlives the app unless it + // unregisters: an app that takes this must release it on shutdown, or the next one inherits a + // path whose callbacks point into unloaded memory. + ESP_ELFSYM_EXPORT(esp_vfs_register), + ESP_ELFSYM_EXPORT(esp_vfs_unregister), // esp_netif.h ESP_ELFSYM_EXPORT(esp_netif_get_ip_info), ESP_ELFSYM_EXPORT(esp_netif_get_handle_from_ifkey), @@ -360,6 +391,19 @@ const esp_elfsym main_symbols[] { ESP_ELFSYM_EXPORT(tinfl_decompress), ESP_ELFSYM_EXPORT(tinfl_decompress_mem_to_callback), ESP_ELFSYM_EXPORT(tinfl_decompress_mem_to_mem), + // Compression, the counterpart to the tinfl_* decompression above. Like those, these live in + // the chip's ROM rather than in flash, so exporting them costs nothing. + // + // Note this is miniz, not zlib: ESP-IDF builds it with MINIZ_NO_ZLIB_APIS, so deflate/inflate, + // crc32 and the gz* file API do not exist anywhere in the firmware. An app wanting gzip files + // has to use the tdefl_/tinfl_ interfaces directly, or vendor zlib itself. + ESP_ELFSYM_EXPORT(tdefl_init), + ESP_ELFSYM_EXPORT(tdefl_compress), + ESP_ELFSYM_EXPORT(tdefl_compress_buffer), + ESP_ELFSYM_EXPORT(tdefl_compress_mem_to_mem), + ESP_ELFSYM_EXPORT(tdefl_compress_mem_to_output), + ESP_ELFSYM_EXPORT(tdefl_get_adler32), + ESP_ELFSYM_EXPORT(tdefl_get_prev_return_status), // ledc ESP_ELFSYM_EXPORT(ledc_update_duty), ESP_ELFSYM_EXPORT(ledc_set_freq), diff --git a/TactilityKernel/include/tactility/drivers/display.h b/TactilityKernel/include/tactility/drivers/display.h index 02b90f151..97de217e4 100644 --- a/TactilityKernel/include/tactility/drivers/display.h +++ b/TactilityKernel/include/tactility/drivers/display.h @@ -23,7 +23,13 @@ enum DisplayCapability { DISPLAY_CAPABILITY_SLEEP = 1 << 6, DISPLAY_CAPABILITY_REQUIRES_FULL_FRAME = 1 << 7, /** Can be used by e-paper with pointer devices for long click timing changes. */ - DISPLAY_CAPABILITY_SLOW_REFRESH = 1 << 8 + DISPLAY_CAPABILITY_SLOW_REFRESH = 1 << 8, + /** + * Promise that draw_bitmap() never DMAs directly from the color_data pointer it's given (e.g. + * it copies/converts into its own buffer first). Lets the LVGL bridge allocate this display's + * draw buffer(s) from non-DMA-capable memory instead of forcing scarce internal RAM. + */ + DISPLAY_CAPABILITY_PREFER_EXTERNAL_RAM = 1 << 9 }; /** @@ -36,6 +42,11 @@ enum DisplayColorFormat { DISPLAY_COLOR_FORMAT_RGB565 = 0x3, DISPLAY_COLOR_FORMAT_RGB565_SWAPPED = 0x4, DISPLAY_COLOR_FORMAT_RGB888 = 0x5, + // 8 bpp luminance, 0x00 = black, 0xFF = white (matches LVGL's LV_COLOR_FORMAT_L8). Unlike + // MONOCHROME, the LVGL bridge does not force full-frame rendering for this format, so drivers + // that want real partial/tile updates (e.g. grayscale e-paper panels) should report this + // instead of MONOCHROME even if they intend to threshold down to black/white themselves. + DISPLAY_COLOR_FORMAT_GRAYSCALE8 = 0x6, }; /** diff --git a/TactilityKernel/include/tactility/drivers/keyboard.h b/TactilityKernel/include/tactility/drivers/keyboard.h index a4af4c788..f1390f22d 100644 --- a/TactilityKernel/include/tactility/drivers/keyboard.h +++ b/TactilityKernel/include/tactility/drivers/keyboard.h @@ -24,6 +24,25 @@ struct KeyboardKeyData { * immediately to drain it. False if this was the last pending event. */ bool continue_reading; + /** + * @brief True if Ctrl was held when this key was pressed. + * + * Reported separately rather than folded into `key` because the two encodings collide: the C0 + * control codes a terminal expects for Ctrl chords (Ctrl+C is 0x03, Ctrl+K is 0x0B, ...) overlap + * the LVGL key constants drivers emit in the same field (LV_KEY_END is 3, LV_KEY_PREV is 11, + * LV_KEY_UP is 17, ...), so a single uint32_t cannot express both. Consumers that want control + * codes derive them here, e.g. + * `((key >= 'a' && key <= 'z') || (key >= 'A' && key <= 'Z')) ? (key & 0x1F) : key` + * when ctrl is set. + * + * Drivers whose hardware cannot report Ctrl leave this false. + */ + bool ctrl; + /** + * @brief True if Alt was held when this key was pressed. See ctrl for why modifiers are reported + * separately. Drivers whose hardware cannot report Alt leave this false. + */ + bool alt; }; /** diff --git a/TactilityKernel/include/tactility/drivers/usb_host_hid.h b/TactilityKernel/include/tactility/drivers/usb_host_hid.h index c7b2b111b..abba9c95c 100644 --- a/TactilityKernel/include/tactility/drivers/usb_host_hid.h +++ b/TactilityKernel/include/tactility/drivers/usb_host_hid.h @@ -43,7 +43,18 @@ typedef enum { typedef struct { UsbHidEventType type; union { - struct { uint32_t key_code; bool pressed; } key; + /** + * @brief A key press or release. + * + * `ctrl` and `alt` report the modifiers separately rather than folding them into + * `key_code`, because the two encodings collide: the C0 control codes a terminal expects + * for Ctrl chords (Ctrl+C is 0x03, Ctrl+K is 0x0B) overlap the UsbHidKey constants above + * (USB_HID_KEY_END is 3, USB_HID_KEY_PREV is 11). A consumer wanting control codes derives + * them here, e.g. + * `((key_code >= 'a' && key_code <= 'z') || (key_code >= 'A' && key_code <= 'Z')) ? + * (key_code & 0x1F) : key_code`. + */ + struct { uint32_t key_code; bool pressed; bool ctrl; bool alt; } key; struct { int32_t dx; int32_t dy; } mouse_move; struct { bool button1; bool button2; } mouse_btn; struct { int32_t delta; } scroll; diff --git a/TactilityKernel/source/drivers/keyboard.cpp b/TactilityKernel/source/drivers/keyboard.cpp index 6bb02bf40..8c14bcd28 100644 --- a/TactilityKernel/source/drivers/keyboard.cpp +++ b/TactilityKernel/source/drivers/keyboard.cpp @@ -9,6 +9,12 @@ extern "C" { error_t keyboard_read_key(Device* device, KeyboardKeyData* data) { const auto* driver = device_get_driver(device); + + // Default the modifier fields here rather than in each driver: only drivers whose hardware can + // report modifiers set them, and the rest would otherwise leave whatever the caller's stack held. + data->ctrl = false; + data->alt = false; + return KEYBOARD_DRIVER_API(driver)->read_key(device, data); } diff --git a/TactilityKernel/source/symbols.c b/TactilityKernel/source/symbols.c index 48c500737..418b116f8 100644 --- a/TactilityKernel/source/symbols.c +++ b/TactilityKernel/source/symbols.c @@ -171,6 +171,7 @@ const struct ModuleSymbol KERNEL_SYMBOLS[] = { DEFINE_MODULE_SYMBOL(file_system_unmount), DEFINE_MODULE_SYMBOL(file_system_is_mounted), DEFINE_MODULE_SYMBOL(file_system_get_path), + DEFINE_MODULE_SYMBOL(file_system_for_each), // memory DEFINE_MODULE_SYMBOL(MEMORY_POLICY_DEFAULT), DEFINE_MODULE_SYMBOL(memory_print_stats),