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 d4604c897..6c6152f73 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/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/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/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/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/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/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/Tactility/Source/Tactility.cpp b/Tactility/Source/Tactility.cpp
index cf683f9e2..6683dc191 100644
--- a/Tactility/Source/Tactility.cpp
+++ b/Tactility/Source/Tactility.cpp
@@ -33,6 +33,7 @@
#include
#include
#include
+#include
#include
@@ -40,6 +41,9 @@
#include
#include
+#include
+#include
+#include
#include
#include
@@ -379,6 +383,33 @@ static lv_obj_t* windowManagerScreenInit(lv_obj_t* root) {
return app_container;
}
+#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() {
window_manager_configure(windowManagerScreenInit);
check(module_ensure_started(&lvgl_window_manager_module) == ERROR_NONE);
@@ -401,6 +432,10 @@ static void onLvglStarted() {
lvgl::startUsbHidInput();
lvgl::initTrackball();
+#ifdef CONFIG_TT_TOUCH_CALIBRATION_SUPPORTED
+ applySavedTouchCalibration();
+#endif
+
memory_print_stats();
}
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/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 74f8bea41..3a1007d57 100644
--- a/TactilityC/Source/tt_init.cpp
+++ b/TactilityC/Source/tt_init.cpp
@@ -30,6 +30,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -86,6 +87,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),
@@ -191,6 +193,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),
@@ -236,6 +242,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),
@@ -253,6 +260,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),
@@ -295,9 +309,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),
@@ -328,6 +359,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 6928bd486..35ba0a754 100644
--- a/TactilityKernel/source/symbols.c
+++ b/TactilityKernel/source/symbols.c
@@ -174,6 +174,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),
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-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,
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,