From 58ac4b78d806c964d9ee8de624d613090b58d87a Mon Sep 17 00:00:00 2001
From: AdaSzi115026 <81822538+AdaSzi@users.noreply.github.com>
Date: Mon, 5 Oct 2026 01:08:58 +0200
Subject: [PATCH 1/5] Support IO expander pins in the GT911 driver
Reset and interrupt pins on an IO expander were passed to esp_lcd_touch
as native GPIO numbers. The driver now resets the controller itself for
such pins, holding INT low so it comes up at address 0x5D. Native pins
behave as before.
---
.../gt911-module/bindings/goodix,gt911.yaml | 3 +-
Drivers/gt911-module/source/gt911.cpp | 73 ++++++++++++++++++-
2 files changed, 72 insertions(+), 4 deletions(-)
diff --git a/Drivers/gt911-module/bindings/goodix,gt911.yaml b/Drivers/gt911-module/bindings/goodix,gt911.yaml
index e5e8b58b7..8bb094cb2 100644
--- a/Drivers/gt911-module/bindings/goodix,gt911.yaml
+++ b/Drivers/gt911-module/bindings/goodix,gt911.yaml
@@ -33,7 +33,8 @@ properties:
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).
+ reset line, e.g. Tulip 4 R11, use ACTIVE_HIGH). A pin on an IO expander is driven
+ active-low at physical levels, with the interrupt pin held low during reset (address 0x5D).
pin-interrupt:
type: phandles
default: GPIO_PIN_SPEC_NONE
diff --git a/Drivers/gt911-module/source/gt911.cpp b/Drivers/gt911-module/source/gt911.cpp
index 621e44694..d6feadc71 100644
--- a/Drivers/gt911-module/source/gt911.cpp
+++ b/Drivers/gt911-module/source/gt911.cpp
@@ -69,6 +69,70 @@ static error_t reset_controller_pin(const GpioPinSpec& pin, uint8_t pulses) {
return ERROR_NONE;
}
+static bool is_expander_pin(const GpioPinSpec& pin) {
+ return pin.gpio_controller != nullptr && !driver_is_compatible(device_get_driver(pin.gpio_controller), "espressif,esp32-gpio");
+}
+
+// esp_lcd_touch only accepts native pins, so the reset is done here. INT is held low while reset
+// is released, which selects address 0x5D. Physical levels are used because IO expanders reject
+// ACTIVE_LOW on outputs.
+static error_t reset_with_expander_pins(const GpioPinSpec& reset_pin, const GpioPinSpec& interrupt_pin) {
+ GpioDescriptor* interrupt = nullptr;
+ if (interrupt_pin.gpio_controller != nullptr) {
+ interrupt = gpio_descriptor_acquire(interrupt_pin.gpio_controller, interrupt_pin.pin, GPIO_FLAG_DIRECTION_OUTPUT, GPIO_OWNER_GPIO);
+ if (interrupt == nullptr) {
+ LOG_E(TAG, "Failed to acquire interrupt pin");
+ return ERROR_RESOURCE;
+ }
+ }
+
+ GpioDescriptor* reset = nullptr;
+ if (reset_pin.gpio_controller != nullptr) {
+ reset = gpio_descriptor_acquire(reset_pin.gpio_controller, reset_pin.pin, GPIO_FLAG_DIRECTION_OUTPUT, GPIO_OWNER_GPIO);
+ if (reset == nullptr) {
+ LOG_E(TAG, "Failed to acquire reset pin");
+ if (interrupt != nullptr) {
+ gpio_descriptor_release(interrupt);
+ }
+ return ERROR_RESOURCE;
+ }
+ }
+
+ error_t error = ERROR_NONE;
+ if (interrupt != nullptr) {
+ error = gpio_descriptor_set_level(interrupt, false);
+ }
+ if (error == ERROR_NONE && reset != nullptr) {
+ error = gpio_descriptor_set_level(reset, false);
+ if (error == ERROR_NONE) {
+ vTaskDelay(pdMS_TO_TICKS(10));
+ error = gpio_descriptor_set_level(reset, true);
+ }
+ }
+ if (error == ERROR_NONE) {
+ vTaskDelay(pdMS_TO_TICKS(10));
+ }
+
+ if (interrupt != nullptr) {
+ error_t release_error = gpio_descriptor_set_flags(interrupt, GPIO_FLAG_DIRECTION_INPUT);
+ if (error == ERROR_NONE) {
+ error = release_error;
+ }
+ gpio_descriptor_release(interrupt);
+ }
+ if (reset != nullptr) {
+ gpio_descriptor_release(reset);
+ }
+
+ if (error != ERROR_NONE) {
+ LOG_E(TAG, "Failed to reset controller");
+ return error;
+ }
+
+ vTaskDelay(pdMS_TO_TICKS(60));
+ return ERROR_NONE;
+}
+
// region Driver lifecycle
// GT911's I2C address depends on the controller's INT pin level at power-up (board-strapped, not
@@ -149,7 +213,10 @@ static error_t start(Device* device) {
return ERROR_OUT_OF_MEMORY;
}
- error_t error = reset_controller_pin(config->pin_reset, config->reset_pulses);
+ bool expander_pins = is_expander_pin(config->pin_reset) || is_expander_pin(config->pin_interrupt);
+ error_t error = expander_pins
+ ? reset_with_expander_pins(config->pin_reset, config->pin_interrupt)
+ : reset_controller_pin(config->pin_reset, config->reset_pulses);
if (error != ERROR_NONE) {
free(internal);
return error;
@@ -164,8 +231,8 @@ static error_t start(Device* device) {
esp_lcd_touch_config_t touch_config = {
.x_max = config->x_max,
.y_max = config->y_max,
- .rst_gpio_num = pin_or_nc(config->pin_reset),
- .int_gpio_num = pin_or_nc(config->pin_interrupt),
+ .rst_gpio_num = expander_pins ? GPIO_NUM_NC : pin_or_nc(config->pin_reset),
+ .int_gpio_num = expander_pins ? GPIO_NUM_NC : pin_or_nc(config->pin_interrupt),
// Reset polarity comes from the pin_reset descriptor's ACTIVE_HIGH/ACTIVE_LOW flag; the
// interrupt line is fixed active-low in hardware.
.levels = {
From 5a0147acb2d26a4a7fec1915483dc22567fca6b1 Mon Sep 17 00:00:00 2001
From: AdaSzi115026 <81822538+AdaSzi@users.noreply.github.com>
Date: Mon, 5 Oct 2026 01:08:58 +0200
Subject: [PATCH 2/5] Add Seeed Studio Wio Tracker L2 Pro
New driver modules:
- nv3031b-module: NV3031B display over QSPI. The esp_lcd panel is
adapted from Espressif's esp_lcd_sh8601 (Apache-2.0), the init
sequence is from LovyanGFX (BSD-2-Clause).
- lp5814-module: TI LP5814 LED driver as display backlight.
The device folder is dts-only.
Working, tested on hardware: display (320x240 landscape), touch,
backlight brightness, SD card (SDMMC 1-bit), WiFi, GPS detection
(L76K on UART1).
Not implemented yet: battery voltage (ADS1115), buttons (BOOT, WAKE),
audio (ES8311 speaker, ES7243E microphone), LoRa (SX1262), user LED,
Grove port, USB-C controller (AW35615).
---
.../LICENSE-Apache-2.0.md | 195 +++++++++
.../device.properties | 21 +
Devices/seeed-wio-tracker-l2-pro/module.yaml | 9 +
.../seeed,wio-tracker-l2-pro.dts | 178 ++++++++
Drivers/lp5814-module/CMakeLists.txt | 11 +
Drivers/lp5814-module/LICENSE-Apache-2.0.md | 195 +++++++++
Drivers/lp5814-module/README.md | 7 +
Drivers/lp5814-module/bindings/ti,lp5814.yaml | 27 ++
.../lp5814-module/include/bindings/lp5814.h | 7 +
.../lp5814-module/include/drivers/lp5814.h | 22 +
Drivers/lp5814-module/include/lp5814_module.h | 14 +
Drivers/lp5814-module/module.yaml | 3 +
Drivers/lp5814-module/source/lp5814.cpp | 165 ++++++++
Drivers/lp5814-module/source/module.cpp | 23 ++
Drivers/nv3031b-module/CMakeLists.txt | 11 +
Drivers/nv3031b-module/LICENSE-Apache-2.0.md | 195 +++++++++
.../nv3031b-module/LICENSE-BSD-2-Clause.md | 35 ++
Drivers/nv3031b-module/README.md | 12 +
.../bindings/newvision,nv3031b.yaml | 59 +++
.../nv3031b-module/include/bindings/nv3031b.h | 7 +
.../nv3031b-module/include/drivers/nv3031b.h | 34 ++
.../nv3031b-module/include/esp_lcd_nv3031b.h | 47 +++
.../nv3031b-module/include/nv3031b_module.h | 14 +
Drivers/nv3031b-module/module.yaml | 4 +
.../nv3031b-module/source/esp_lcd_nv3031b.c | 304 ++++++++++++++
Drivers/nv3031b-module/source/module.cpp | 23 ++
Drivers/nv3031b-module/source/nv3031b.cpp | 379 ++++++++++++++++++
.../nv3031b-module/source/nv3031b_init_cmds.h | 34 ++
THIRD-PARTY-NOTICES.md | 16 +
29 files changed, 2051 insertions(+)
create mode 100644 Devices/seeed-wio-tracker-l2-pro/LICENSE-Apache-2.0.md
create mode 100644 Devices/seeed-wio-tracker-l2-pro/device.properties
create mode 100644 Devices/seeed-wio-tracker-l2-pro/module.yaml
create mode 100644 Devices/seeed-wio-tracker-l2-pro/seeed,wio-tracker-l2-pro.dts
create mode 100644 Drivers/lp5814-module/CMakeLists.txt
create mode 100644 Drivers/lp5814-module/LICENSE-Apache-2.0.md
create mode 100644 Drivers/lp5814-module/README.md
create mode 100644 Drivers/lp5814-module/bindings/ti,lp5814.yaml
create mode 100644 Drivers/lp5814-module/include/bindings/lp5814.h
create mode 100644 Drivers/lp5814-module/include/drivers/lp5814.h
create mode 100644 Drivers/lp5814-module/include/lp5814_module.h
create mode 100644 Drivers/lp5814-module/module.yaml
create mode 100644 Drivers/lp5814-module/source/lp5814.cpp
create mode 100644 Drivers/lp5814-module/source/module.cpp
create mode 100644 Drivers/nv3031b-module/CMakeLists.txt
create mode 100644 Drivers/nv3031b-module/LICENSE-Apache-2.0.md
create mode 100644 Drivers/nv3031b-module/LICENSE-BSD-2-Clause.md
create mode 100644 Drivers/nv3031b-module/README.md
create mode 100644 Drivers/nv3031b-module/bindings/newvision,nv3031b.yaml
create mode 100644 Drivers/nv3031b-module/include/bindings/nv3031b.h
create mode 100644 Drivers/nv3031b-module/include/drivers/nv3031b.h
create mode 100644 Drivers/nv3031b-module/include/esp_lcd_nv3031b.h
create mode 100644 Drivers/nv3031b-module/include/nv3031b_module.h
create mode 100644 Drivers/nv3031b-module/module.yaml
create mode 100644 Drivers/nv3031b-module/source/esp_lcd_nv3031b.c
create mode 100644 Drivers/nv3031b-module/source/module.cpp
create mode 100644 Drivers/nv3031b-module/source/nv3031b.cpp
create mode 100644 Drivers/nv3031b-module/source/nv3031b_init_cmds.h
diff --git a/Devices/seeed-wio-tracker-l2-pro/LICENSE-Apache-2.0.md b/Devices/seeed-wio-tracker-l2-pro/LICENSE-Apache-2.0.md
new file mode 100644
index 000000000..f5f4b8b5e
--- /dev/null
+++ b/Devices/seeed-wio-tracker-l2-pro/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/seeed-wio-tracker-l2-pro/device.properties b/Devices/seeed-wio-tracker-l2-pro/device.properties
new file mode 100644
index 000000000..f71fc09e0
--- /dev/null
+++ b/Devices/seeed-wio-tracker-l2-pro/device.properties
@@ -0,0 +1,21 @@
+general.vendor=Seeed Studio
+general.name=Wio Tracker L2 Pro
+
+apps.launcherAppId=tactility.launcher
+
+hardware.target=ESP32S3
+hardware.flashSize=16MB
+hardware.spiRam=true
+hardware.spiRamMode=OCT
+hardware.spiRamSpeed=80M
+hardware.esptoolFlashFreq=80M
+hardware.tinyUsbMsc=true
+hardware.bluetooth=true
+
+storage.userDataLocation=SD
+
+display.size=3.2"
+display.shape=rectangle
+display.dpi=125
+
+lvgl.colorDepth=16
diff --git a/Devices/seeed-wio-tracker-l2-pro/module.yaml b/Devices/seeed-wio-tracker-l2-pro/module.yaml
new file mode 100644
index 000000000..c6d1e683e
--- /dev/null
+++ b/Devices/seeed-wio-tracker-l2-pro/module.yaml
@@ -0,0 +1,9 @@
+dependencies:
+ - Platforms/platform-esp32
+ - Drivers/tca95xx-16bit-module
+ - Drivers/gt911-module
+ - Drivers/lp5814-module
+ - Drivers/nv3031b-module
+ - Drivers/gps-generic-module
+dts: seeed,wio-tracker-l2-pro.dts
+dts-only: true
diff --git a/Devices/seeed-wio-tracker-l2-pro/seeed,wio-tracker-l2-pro.dts b/Devices/seeed-wio-tracker-l2-pro/seeed,wio-tracker-l2-pro.dts
new file mode 100644
index 000000000..1399af3a3
--- /dev/null
+++ b/Devices/seeed-wio-tracker-l2-pro/seeed,wio-tracker-l2-pro.dts
@@ -0,0 +1,178 @@
+/dts-v1/;
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+// Reference: https://wiki.seeedstudio.com/meshtastic_wio_tracker_l2_intro/
+/ {
+ compatible = "root";
+ model = "Seeed Studio Wio Tracker L2 Pro";
+
+ wifi0 {
+ compatible = "espressif,esp32-wifi-pinned";
+ };
+
+ ble0 {
+ compatible = "espressif,esp32-ble";
+ };
+
+ gpio0 {
+ compatible = "espressif,esp32-gpio";
+ gpio-count = <49>;
+ };
+
+ i2c0 {
+ compatible = "espressif,esp32-i2c-master";
+ port = ;
+ clock-frequency = <100000>;
+ pin-sda = <&gpio0 47 GPIO_FLAG_NONE>;
+ pin-scl = <&gpio0 48 GPIO_FLAG_NONE>;
+
+ // PCA9555, register compatible with the TCA9535
+ pca9555 {
+ compatible = "ti,tca9535";
+ reg = <0x21>;
+
+ lcd_power {
+ compatible = "gpio-hog";
+ pin = <&pca9555 5 GPIO_FLAG_NONE>;
+ mode = ;
+ };
+
+ grove_power {
+ compatible = "gpio-hog";
+ pin = <&pca9555 7 GPIO_FLAG_NONE>;
+ mode = ;
+ };
+
+ // Active high
+ gnss_reset {
+ compatible = "gpio-hog";
+ pin = <&pca9555 9 GPIO_FLAG_NONE>;
+ mode = ;
+ };
+
+ user_led {
+ compatible = "gpio-hog";
+ pin = <&pca9555 10 GPIO_FLAG_NONE>;
+ mode = ;
+ };
+
+ usb_otg {
+ compatible = "gpio-hog";
+ pin = <&pca9555 11 GPIO_FLAG_NONE>;
+ mode = ;
+ };
+
+ speaker_amplifier {
+ compatible = "gpio-hog";
+ pin = <&pca9555 12 GPIO_FLAG_NONE>;
+ mode = ;
+ };
+
+ gnss_power {
+ compatible = "gpio-hog";
+ pin = <&pca9555 13 GPIO_FLAG_NONE>;
+ mode = ;
+ };
+
+ sdcard_power {
+ compatible = "gpio-hog";
+ pin = <&pca9555 14 GPIO_FLAG_NONE>;
+ mode = ;
+ };
+
+ battery_divider {
+ compatible = "gpio-hog";
+ pin = <&pca9555 15 GPIO_FLAG_NONE>;
+ mode = ;
+ };
+ };
+
+ // Native orientation is portrait, the device is used in landscape
+ touch {
+ compatible = "goodix,gt911";
+ reg = <0x5D>;
+ x-max = <240>;
+ y-max = <320>;
+ swap-xy;
+ mirror-y;
+ pin-reset = <&pca9555 8 GPIO_FLAG_NONE>;
+ pin-interrupt = <&pca9555 3 GPIO_FLAG_NONE>;
+ };
+
+ // Started at boot after the display is drawn
+ display_backlight {
+ compatible = "ti,lp5814";
+ reg = <0x2C>;
+ status = "disabled";
+ high-current;
+ dot-current = <200>;
+ };
+ };
+
+ spi1 {
+ compatible = "espressif,esp32-spi";
+ host = ;
+ cs-gpios = <&gpio0 46 GPIO_FLAG_NONE>;
+ pin-sclk = <&gpio0 42 GPIO_FLAG_NONE>;
+ pin-mosi = <&gpio0 41 GPIO_FLAG_NONE>;
+ pin-miso = <&gpio0 40 GPIO_FLAG_NONE>;
+ pin-wp = <&gpio0 39 GPIO_FLAG_NONE>;
+ pin-hd = <&gpio0 38 GPIO_FLAG_NONE>;
+
+ display@0 {
+ compatible = "newvision,nv3031b";
+ horizontal-resolution = <320>;
+ vertical-resolution = <240>;
+ swap-xy;
+ mirror-y;
+ pixel-clock-hz = <40000000>;
+ pin-reset = <&pca9555 6 GPIO_FLAG_NONE>;
+ backlight = <&display_backlight>;
+ };
+ };
+
+ sdmmc0 {
+ compatible = "espressif,esp32-sdmmc";
+ pin-clk = <&gpio0 2 GPIO_FLAG_NONE>;
+ pin-cmd = <&gpio0 3 GPIO_FLAG_NONE>;
+ pin-d0 = <&gpio0 1 GPIO_FLAG_NONE>;
+ slot = ;
+ bus-width = <1>;
+ };
+
+ uart_gps {
+ compatible = "espressif,esp32-uart";
+ port = ;
+ pin-tx = <&gpio0 17 GPIO_FLAG_NONE>;
+ pin-rx = <&gpio0 18 GPIO_FLAG_NONE>;
+
+ gps {
+ compatible = "tactility,gps-generic";
+ status = "disabled";
+ baud-rate = <9600>;
+ };
+ };
+
+ usbdevice0 {
+ compatible = "espressif,esp32-usbdevice";
+
+ usbdevicemsc0 {
+ compatible = "espressif,esp32-usbdevice-msc";
+ };
+ };
+};
diff --git a/Drivers/lp5814-module/CMakeLists.txt b/Drivers/lp5814-module/CMakeLists.txt
new file mode 100644
index 000000000..4b5e6f890
--- /dev/null
+++ b/Drivers/lp5814-module/CMakeLists.txt
@@ -0,0 +1,11 @@
+cmake_minimum_required(VERSION 3.20)
+
+include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake")
+
+file(GLOB_RECURSE SOURCE_FILES "source/*.c*")
+
+tactility_add_module(lp5814-module
+ SRCS ${SOURCE_FILES}
+ INCLUDE_DIRS include/
+ REQUIRES TactilityKernel
+)
diff --git a/Drivers/lp5814-module/LICENSE-Apache-2.0.md b/Drivers/lp5814-module/LICENSE-Apache-2.0.md
new file mode 100644
index 000000000..f5f4b8b5e
--- /dev/null
+++ b/Drivers/lp5814-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/lp5814-module/README.md b/Drivers/lp5814-module/README.md
new file mode 100644
index 000000000..d0cdc8430
--- /dev/null
+++ b/Drivers/lp5814-module/README.md
@@ -0,0 +1,7 @@
+# LP5814 backlight
+
+A backlight driver for the Texas Instruments `LP5814` 4-channel I2C LED driver.
+The selected outputs are driven together with manual PWM dimming.
+Written from the TI datasheet (SNVSCQ0A).
+
+License: [Apache v2.0](LICENSE-Apache-2.0.md)
diff --git a/Drivers/lp5814-module/bindings/ti,lp5814.yaml b/Drivers/lp5814-module/bindings/ti,lp5814.yaml
new file mode 100644
index 000000000..6ab72a48c
--- /dev/null
+++ b/Drivers/lp5814-module/bindings/ti,lp5814.yaml
@@ -0,0 +1,27 @@
+description: >
+ Texas Instruments LP5814 4-channel LED driver used as a display backlight.
+ The outputs selected by channels are driven together with manual PWM dimming.
+
+include: ["i2c-device.yaml"]
+
+compatible: "ti,lp5814"
+
+bus: i2c
+
+properties:
+ channels:
+ type: int
+ default: 15
+ description: Bit mask of the outputs OUT0 to OUT3 that drive the backlight
+ high-current:
+ type: boolean
+ default: false
+ description: Use the 51 mA maximum output current range instead of 25.5 mA
+ dot-current:
+ type: int
+ default: 255
+ description: Output current as a fraction of the maximum current [0,255]
+ brightness-default:
+ type: int
+ default: 255
+ description: Default brightness level, applied by set_brightness_default()
diff --git a/Drivers/lp5814-module/include/bindings/lp5814.h b/Drivers/lp5814-module/include/bindings/lp5814.h
new file mode 100644
index 000000000..2532ecbc0
--- /dev/null
+++ b/Drivers/lp5814-module/include/bindings/lp5814.h
@@ -0,0 +1,7 @@
+// SPDX-License-Identifier: Apache-2.0
+#pragma once
+
+#include
+#include
+
+DEFINE_DEVICETREE(lp5814, struct Lp5814Config)
diff --git a/Drivers/lp5814-module/include/drivers/lp5814.h b/Drivers/lp5814-module/include/drivers/lp5814.h
new file mode 100644
index 000000000..076f0eea7
--- /dev/null
+++ b/Drivers/lp5814-module/include/drivers/lp5814.h
@@ -0,0 +1,22 @@
+// SPDX-License-Identifier: Apache-2.0
+#pragma once
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include
+#include
+
+struct Lp5814Config {
+ uint8_t address;
+ // Bit mask of OUT0 to OUT3
+ uint8_t channels;
+ bool high_current;
+ uint8_t dot_current;
+ uint8_t brightness_default;
+};
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/Drivers/lp5814-module/include/lp5814_module.h b/Drivers/lp5814-module/include/lp5814_module.h
new file mode 100644
index 000000000..f29dbbb3d
--- /dev/null
+++ b/Drivers/lp5814-module/include/lp5814_module.h
@@ -0,0 +1,14 @@
+// SPDX-License-Identifier: Apache-2.0
+#pragma once
+
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+extern struct Module lp5814_module;
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/Drivers/lp5814-module/module.yaml b/Drivers/lp5814-module/module.yaml
new file mode 100644
index 000000000..a07d6f334
--- /dev/null
+++ b/Drivers/lp5814-module/module.yaml
@@ -0,0 +1,3 @@
+dependencies:
+ - TactilityKernel
+bindings: bindings
diff --git a/Drivers/lp5814-module/source/lp5814.cpp b/Drivers/lp5814-module/source/lp5814.cpp
new file mode 100644
index 000000000..fb0477976
--- /dev/null
+++ b/Drivers/lp5814-module/source/lp5814.cpp
@@ -0,0 +1,165 @@
+// SPDX-License-Identifier: Apache-2.0
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+#define TAG "LP5814"
+#define GET_CONFIG(device) (static_cast((device)->config))
+#define GET_INTERNAL(device) (static_cast(device_get_driver_data(device)))
+
+static const TickType_t I2C_TIMEOUT = pdMS_TO_TICKS(50);
+
+static constexpr uint8_t REG_CHIP_EN = 0x00;
+static constexpr uint8_t REG_DEV_CONFIG0 = 0x01;
+static constexpr uint8_t REG_DEV_CONFIG1 = 0x02;
+static constexpr uint8_t REG_RESET_CMD = 0x0E;
+static constexpr uint8_t REG_UPDATE_CMD = 0x0F;
+static constexpr uint8_t REG_OUT0_DC = 0x14;
+static constexpr uint8_t REG_OUT0_MANUAL_PWM = 0x18;
+
+static constexpr uint8_t RESET_KEY = 0xCC;
+static constexpr uint8_t UPDATE_KEY = 0x55;
+static constexpr uint8_t CHANNEL_COUNT = 4;
+
+extern "C" {
+
+struct Lp5814Internal {
+ uint8_t brightness;
+};
+
+static error_t write_register(Device* device, uint8_t reg, uint8_t value) {
+ return i2c_controller_register8_set(device_get_parent(device), GET_CONFIG(device)->address, reg, value, I2C_TIMEOUT);
+}
+
+static error_t write_per_channel(Device* device, uint8_t first_reg, uint8_t value) {
+ const auto* config = GET_CONFIG(device);
+ uint8_t values[CHANNEL_COUNT];
+ for (uint8_t i = 0; i < CHANNEL_COUNT; i++) {
+ values[i] = (config->channels & (1U << i)) != 0 ? value : 0;
+ }
+ return i2c_controller_write_register(device_get_parent(device), config->address, first_reg, values, CHANNEL_COUNT, I2C_TIMEOUT);
+}
+
+// Registers 0x01 to 0x05 only take effect after the update command
+static error_t configure(Device* device) {
+ const auto* config = GET_CONFIG(device);
+
+ error_t error = write_register(device, REG_RESET_CMD, RESET_KEY);
+ if (error == ERROR_NONE) {
+ delay_millis(1);
+ error = write_register(device, REG_CHIP_EN, 0x01);
+ }
+ if (error == ERROR_NONE) {
+ error = write_register(device, REG_DEV_CONFIG0, config->high_current ? 0x01 : 0x00);
+ }
+ if (error == ERROR_NONE) {
+ error = write_register(device, REG_DEV_CONFIG1, config->channels & 0x0F);
+ }
+ if (error == ERROR_NONE) {
+ error = write_register(device, REG_UPDATE_CMD, UPDATE_KEY);
+ }
+ if (error == ERROR_NONE) {
+ error = write_per_channel(device, REG_OUT0_DC, config->dot_current);
+ }
+ return error;
+}
+
+// region BacklightApi
+
+static error_t lp5814_set_brightness(Device* device, uint8_t brightness) {
+ error_t error = write_per_channel(device, REG_OUT0_MANUAL_PWM, brightness);
+ if (error != ERROR_NONE) {
+ return error;
+ }
+ GET_INTERNAL(device)->brightness = brightness;
+ return ERROR_NONE;
+}
+
+static error_t lp5814_set_brightness_default(Device* device) {
+ return lp5814_set_brightness(device, GET_CONFIG(device)->brightness_default);
+}
+
+static error_t lp5814_get_brightness(Device* device, uint8_t* out_brightness) {
+ *out_brightness = GET_INTERNAL(device)->brightness;
+ return ERROR_NONE;
+}
+
+static uint8_t lp5814_get_min_brightness(Device*) {
+ return 0;
+}
+
+static uint8_t lp5814_get_max_brightness(Device*) {
+ return 255;
+}
+
+// endregion
+
+static constexpr BacklightApi LP5814_BACKLIGHT_API = {
+ .set_brightness = lp5814_set_brightness,
+ .set_brightness_default = lp5814_set_brightness_default,
+ .get_brightness = lp5814_get_brightness,
+ .get_min_brightness = lp5814_get_min_brightness,
+ .get_max_brightness = lp5814_get_max_brightness,
+};
+
+// region Driver lifecycle
+
+static error_t start(Device* device) {
+ check(device_get_type(device_get_parent(device)) == &I2C_CONTROLLER_TYPE);
+
+ auto* internal = new(std::nothrow) Lp5814Internal { .brightness = 0 };
+ if (internal == nullptr) {
+ return ERROR_OUT_OF_MEMORY;
+ }
+ device_set_driver_data(device, internal);
+
+ error_t error = configure(device);
+ if (error == ERROR_NONE) {
+ error = lp5814_set_brightness_default(device);
+ }
+ if (error != ERROR_NONE) {
+ LOG_E(TAG, "Failed to configure");
+ device_set_driver_data(device, nullptr);
+ delete internal;
+ return error;
+ }
+
+ return ERROR_NONE;
+}
+
+static error_t stop(Device* device) {
+ // Allowed to fail, the device is stopped either way
+ lp5814_set_brightness(device, 0);
+ write_register(device, REG_CHIP_EN, 0x00);
+
+ auto* internal = GET_INTERNAL(device);
+ device_set_driver_data(device, nullptr);
+ delete internal;
+
+ return ERROR_NONE;
+}
+
+// endregion
+
+Driver lp5814_driver = {
+ .name = "lp5814",
+ .compatible = (const char*[]) { "ti,lp5814", nullptr },
+ .start_device = start,
+ .stop_device = stop,
+ .probe = nullptr,
+ .api = &LP5814_BACKLIGHT_API,
+ .device_type = &BACKLIGHT_TYPE,
+ .owner = &lp5814_module,
+ .internal = nullptr
+};
+
+}
diff --git a/Drivers/lp5814-module/source/module.cpp b/Drivers/lp5814-module/source/module.cpp
new file mode 100644
index 000000000..626c09a3e
--- /dev/null
+++ b/Drivers/lp5814-module/source/module.cpp
@@ -0,0 +1,23 @@
+// SPDX-License-Identifier: Apache-2.0
+#include
+#include
+
+extern "C" {
+
+extern Driver lp5814_driver;
+
+static Driver* const lp5814_drivers[] = {
+ &lp5814_driver,
+ nullptr
+};
+
+Module lp5814_module = {
+ .name = "lp5814",
+ .start = nullptr,
+ .stop = nullptr,
+ .drivers = lp5814_drivers,
+ .symbols = nullptr,
+ .internal = nullptr
+};
+
+} // extern "C"
diff --git a/Drivers/nv3031b-module/CMakeLists.txt b/Drivers/nv3031b-module/CMakeLists.txt
new file mode 100644
index 000000000..24af10db8
--- /dev/null
+++ b/Drivers/nv3031b-module/CMakeLists.txt
@@ -0,0 +1,11 @@
+cmake_minimum_required(VERSION 3.20)
+
+include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake")
+
+file(GLOB_RECURSE SOURCE_FILES "source/*.c*")
+
+tactility_add_module(nv3031b-module
+ SRCS ${SOURCE_FILES}
+ INCLUDE_DIRS include/
+ REQUIRES TactilityKernel platform-esp32 esp_lcd
+)
diff --git a/Drivers/nv3031b-module/LICENSE-Apache-2.0.md b/Drivers/nv3031b-module/LICENSE-Apache-2.0.md
new file mode 100644
index 000000000..f5f4b8b5e
--- /dev/null
+++ b/Drivers/nv3031b-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/nv3031b-module/LICENSE-BSD-2-Clause.md b/Drivers/nv3031b-module/LICENSE-BSD-2-Clause.md
new file mode 100644
index 000000000..5e2022254
--- /dev/null
+++ b/Drivers/nv3031b-module/LICENSE-BSD-2-Clause.md
@@ -0,0 +1,35 @@
+# BSD 2-Clause License (FreeBSD License)
+
+Applies to `source/nv3031b_init_cmds.h`, which contains the initialization sequence from [LovyanGFX](https://github.com/lovyan03/LovyanGFX) (`src/lgfx/v1/panel/Panel_NV3031B.hpp`).
+
+```
+Software License Agreement (FreeBSD License)
+
+Copyright (c) 2020 lovyan03 (https://github.com/lovyan03)
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+The views and conclusions contained in the software and documentation are those
+of the authors and should not be interpreted as representing official policies,
+either expressed or implied, of the FreeBSD Project.
+```
diff --git a/Drivers/nv3031b-module/README.md b/Drivers/nv3031b-module/README.md
new file mode 100644
index 000000000..526cea55d
--- /dev/null
+++ b/Drivers/nv3031b-module/README.md
@@ -0,0 +1,12 @@
+# NV3031B display
+
+A driver for the `NV3031B` display panel by NewVision, over QSPI.
+
+The esp_lcd panel (`esp_lcd_nv3031b.c`) is adapted from Espressif's
+[esp_lcd_sh8601](https://github.com/espressif/esp-iot-solution/tree/master/components/display/lcd/esp_lcd_sh8601),
+which uses the same QSPI command framing.
+
+License:
+
+- [Apache v2.0](LICENSE-Apache-2.0.md) for all files except the one below
+- [BSD 2-Clause](LICENSE-BSD-2-Clause.md) for `source/nv3031b_init_cmds.h`, the initialization sequence from [LovyanGFX](https://github.com/lovyan03/LovyanGFX)
diff --git a/Drivers/nv3031b-module/bindings/newvision,nv3031b.yaml b/Drivers/nv3031b-module/bindings/newvision,nv3031b.yaml
new file mode 100644
index 000000000..7cc6854f3
--- /dev/null
+++ b/Drivers/nv3031b-module/bindings/newvision,nv3031b.yaml
@@ -0,0 +1,59 @@
+description: NewVision NV3031B display panel over QSPI
+
+bus: spi
+
+compatible: "newvision,nv3031b"
+
+properties:
+ horizontal-resolution:
+ type: int
+ required: true
+ description: Horizontal resolution in pixels
+ vertical-resolution:
+ type: int
+ required: true
+ description: Vertical resolution in pixels
+ gap-x:
+ type: int
+ default: 0
+ description: X offset applied to all draw operations
+ gap-y:
+ type: int
+ default: 0
+ description: Y offset applied to all draw operations
+ swap-xy:
+ type: boolean
+ default: false
+ description: Swap the X and Y axes
+ mirror-x:
+ type: boolean
+ default: false
+ description: Mirror the X axis
+ mirror-y:
+ type: boolean
+ default: false
+ description: Mirror the Y axis
+ invert-color:
+ type: boolean
+ default: false
+ description: Invert the panel's color output
+ bgr-order:
+ type: boolean
+ default: false
+ description: Use BGR element order instead of RGB
+ pixel-clock-hz:
+ type: int
+ default: 40000000
+ description: SPI clock frequency in Hz
+ transaction-queue-depth:
+ type: int
+ default: 10
+ description: Size of the internal SPI transaction queue
+ pin-reset:
+ type: phandles
+ default: GPIO_PIN_SPEC_NONE
+ description: Active-low reset pin. Pins on an IO expander are supported.
+ backlight:
+ type: phandle
+ default: "NULL"
+ description: Optional reference to this display's backlight device
diff --git a/Drivers/nv3031b-module/include/bindings/nv3031b.h b/Drivers/nv3031b-module/include/bindings/nv3031b.h
new file mode 100644
index 000000000..31fffa69e
--- /dev/null
+++ b/Drivers/nv3031b-module/include/bindings/nv3031b.h
@@ -0,0 +1,7 @@
+// SPDX-License-Identifier: Apache-2.0
+#pragma once
+
+#include
+#include
+
+DEFINE_DEVICETREE(nv3031b, struct Nv3031bConfig)
diff --git a/Drivers/nv3031b-module/include/drivers/nv3031b.h b/Drivers/nv3031b-module/include/drivers/nv3031b.h
new file mode 100644
index 000000000..4f5b81e33
--- /dev/null
+++ b/Drivers/nv3031b-module/include/drivers/nv3031b.h
@@ -0,0 +1,34 @@
+// SPDX-License-Identifier: Apache-2.0
+#pragma once
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include
+#include
+
+#include
+#include
+
+struct Nv3031bConfig {
+ uint16_t horizontal_resolution;
+ uint16_t vertical_resolution;
+ int32_t gap_x;
+ int32_t gap_y;
+ bool swap_xy;
+ bool mirror_x;
+ bool mirror_y;
+ bool invert_color;
+ bool bgr_order;
+ uint32_t pixel_clock_hz;
+ uint8_t transaction_queue_depth;
+ // Active-low reset line, may be on an IO expander.
+ struct GpioPinSpec pin_reset;
+ // Optional reference to this display's backlight device, NULL if none.
+ struct Device* backlight;
+};
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/Drivers/nv3031b-module/include/esp_lcd_nv3031b.h b/Drivers/nv3031b-module/include/esp_lcd_nv3031b.h
new file mode 100644
index 000000000..bf3e3dc00
--- /dev/null
+++ b/Drivers/nv3031b-module/include/esp_lcd_nv3031b.h
@@ -0,0 +1,47 @@
+/*
+ * SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD
+ * SPDX-FileContributor: Adapted from esp_lcd_sh8601 for the NV3031B
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+#pragma once
+
+#include
+
+#include "esp_lcd_panel_vendor.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef struct {
+ int cmd;
+ const void *data;
+ size_t data_bytes;
+ unsigned int delay_ms;
+} nv3031b_lcd_init_cmd_t;
+
+/**
+ * @brief Vendor specific configuration, passed as `vendor_config` in `esp_lcd_panel_dev_config_t`.
+ *
+ * The panel is driven over QSPI: commands are sent as `0x02 0x00 0x00` on one line,
+ * pixel data as `0x32 0x00 0x2C 0x00` followed by the data on four lines.
+ */
+typedef struct {
+ const nv3031b_lcd_init_cmd_t *init_cmds; /*!< Initialization sequence, NULL for the built-in one */
+ uint16_t init_cmds_size; /*!< Number of commands in init_cmds */
+} nv3031b_vendor_config_t;
+
+/**
+ * @brief Create an LCD panel for the NV3031B.
+ *
+ * @param[in] io LCD panel IO handle (SPI panel IO in quad mode with 32 bit commands)
+ * @param[in] panel_dev_config General panel device configuration
+ * @param[out] ret_panel Returned LCD panel handle
+ * @return ESP_OK on success, otherwise an error code
+ */
+esp_err_t esp_lcd_new_panel_nv3031b(const esp_lcd_panel_io_handle_t io, const esp_lcd_panel_dev_config_t *panel_dev_config, esp_lcd_panel_handle_t *ret_panel);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/Drivers/nv3031b-module/include/nv3031b_module.h b/Drivers/nv3031b-module/include/nv3031b_module.h
new file mode 100644
index 000000000..00fac77de
--- /dev/null
+++ b/Drivers/nv3031b-module/include/nv3031b_module.h
@@ -0,0 +1,14 @@
+// SPDX-License-Identifier: Apache-2.0
+#pragma once
+
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+extern struct Module nv3031b_module;
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/Drivers/nv3031b-module/module.yaml b/Drivers/nv3031b-module/module.yaml
new file mode 100644
index 000000000..cdbdab294
--- /dev/null
+++ b/Drivers/nv3031b-module/module.yaml
@@ -0,0 +1,4 @@
+dependencies:
+ - TactilityKernel
+ - Platforms/platform-esp32
+bindings: bindings
diff --git a/Drivers/nv3031b-module/source/esp_lcd_nv3031b.c b/Drivers/nv3031b-module/source/esp_lcd_nv3031b.c
new file mode 100644
index 000000000..2848e7daa
--- /dev/null
+++ b/Drivers/nv3031b-module/source/esp_lcd_nv3031b.c
@@ -0,0 +1,304 @@
+/*
+ * SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD
+ * SPDX-FileContributor: Adapted from esp_lcd_sh8601 for the NV3031B
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include
+#include
+
+#include "freertos/FreeRTOS.h"
+#include "freertos/task.h"
+#include "driver/gpio.h"
+#include "esp_check.h"
+#include "esp_lcd_panel_interface.h"
+#include "esp_lcd_panel_io.h"
+#include "esp_lcd_panel_vendor.h"
+#include "esp_lcd_panel_ops.h"
+#include "esp_lcd_panel_commands.h"
+#include "esp_log.h"
+
+#include "esp_lcd_nv3031b.h"
+#include "nv3031b_init_cmds.h"
+
+#define LCD_OPCODE_WRITE_CMD (0x02ULL)
+#define LCD_OPCODE_WRITE_COLOR (0x32ULL)
+
+#define NV3031B_RESET_DELAY_MS 120
+#define NV3031B_SLEEP_OUT_DELAY_MS 120
+
+static const char *TAG = "nv3031b";
+
+static esp_err_t panel_nv3031b_del(esp_lcd_panel_t *panel);
+static esp_err_t panel_nv3031b_reset(esp_lcd_panel_t *panel);
+static esp_err_t panel_nv3031b_init(esp_lcd_panel_t *panel);
+static esp_err_t panel_nv3031b_draw_bitmap(esp_lcd_panel_t *panel, int x_start, int y_start, int x_end, int y_end, const void *color_data);
+static esp_err_t panel_nv3031b_invert_color(esp_lcd_panel_t *panel, bool invert_color_data);
+static esp_err_t panel_nv3031b_mirror(esp_lcd_panel_t *panel, bool mirror_x, bool mirror_y);
+static esp_err_t panel_nv3031b_swap_xy(esp_lcd_panel_t *panel, bool swap_axes);
+static esp_err_t panel_nv3031b_set_gap(esp_lcd_panel_t *panel, int x_gap, int y_gap);
+static esp_err_t panel_nv3031b_disp_on_off(esp_lcd_panel_t *panel, bool on_off);
+
+typedef struct {
+ esp_lcd_panel_t base;
+ esp_lcd_panel_io_handle_t io;
+ int reset_gpio_num;
+ int x_gap;
+ int y_gap;
+ uint8_t fb_bits_per_pixel;
+ uint8_t madctl_val;
+ uint8_t colmod_val;
+ const nv3031b_lcd_init_cmd_t *init_cmds;
+ uint16_t init_cmds_size;
+ struct {
+ unsigned int reset_level: 1;
+ } flags;
+} nv3031b_panel_t;
+
+esp_err_t esp_lcd_new_panel_nv3031b(const esp_lcd_panel_io_handle_t io, const esp_lcd_panel_dev_config_t *panel_dev_config, esp_lcd_panel_handle_t *ret_panel)
+{
+ ESP_RETURN_ON_FALSE(io && panel_dev_config && ret_panel, ESP_ERR_INVALID_ARG, TAG, "invalid argument");
+
+ esp_err_t ret = ESP_OK;
+ nv3031b_panel_t *nv3031b = calloc(1, sizeof(nv3031b_panel_t));
+ ESP_GOTO_ON_FALSE(nv3031b, ESP_ERR_NO_MEM, err, TAG, "no mem for nv3031b panel");
+
+ if (panel_dev_config->reset_gpio_num >= 0) {
+ gpio_config_t io_conf = {
+ .mode = GPIO_MODE_OUTPUT,
+ .pin_bit_mask = 1ULL << panel_dev_config->reset_gpio_num,
+ };
+ ESP_GOTO_ON_ERROR(gpio_config(&io_conf), err, TAG, "configure GPIO for RST line failed");
+ }
+
+ switch (panel_dev_config->rgb_ele_order) {
+ case LCD_RGB_ELEMENT_ORDER_RGB:
+ nv3031b->madctl_val = 0;
+ break;
+ case LCD_RGB_ELEMENT_ORDER_BGR:
+ nv3031b->madctl_val = LCD_CMD_BGR_BIT;
+ break;
+ default:
+ ESP_GOTO_ON_FALSE(false, ESP_ERR_NOT_SUPPORTED, err, TAG, "unsupported color element order");
+ break;
+ }
+
+ switch (panel_dev_config->bits_per_pixel) {
+ case 16: // RGB565
+ nv3031b->colmod_val = 0x55;
+ nv3031b->fb_bits_per_pixel = 16;
+ break;
+ case 18: // RGB666, each component in the 6 high bits of a byte
+ nv3031b->colmod_val = 0x66;
+ nv3031b->fb_bits_per_pixel = 24;
+ break;
+ default:
+ ESP_GOTO_ON_FALSE(false, ESP_ERR_NOT_SUPPORTED, err, TAG, "unsupported pixel width");
+ break;
+ }
+
+ nv3031b->io = io;
+ nv3031b->reset_gpio_num = panel_dev_config->reset_gpio_num;
+ nv3031b->flags.reset_level = panel_dev_config->flags.reset_active_high;
+ const nv3031b_vendor_config_t *vendor_config = (const nv3031b_vendor_config_t *)panel_dev_config->vendor_config;
+ if (vendor_config && vendor_config->init_cmds) {
+ nv3031b->init_cmds = vendor_config->init_cmds;
+ nv3031b->init_cmds_size = vendor_config->init_cmds_size;
+ } else {
+ nv3031b->init_cmds = nv3031b_init_cmds_default;
+ nv3031b->init_cmds_size = sizeof(nv3031b_init_cmds_default) / sizeof(nv3031b_lcd_init_cmd_t);
+ }
+ nv3031b->base.del = panel_nv3031b_del;
+ nv3031b->base.reset = panel_nv3031b_reset;
+ nv3031b->base.init = panel_nv3031b_init;
+ nv3031b->base.draw_bitmap = panel_nv3031b_draw_bitmap;
+ nv3031b->base.invert_color = panel_nv3031b_invert_color;
+ nv3031b->base.set_gap = panel_nv3031b_set_gap;
+ nv3031b->base.mirror = panel_nv3031b_mirror;
+ nv3031b->base.swap_xy = panel_nv3031b_swap_xy;
+ nv3031b->base.disp_on_off = panel_nv3031b_disp_on_off;
+ *ret_panel = &(nv3031b->base);
+ ESP_LOGD(TAG, "new nv3031b panel @%p", nv3031b);
+
+ return ESP_OK;
+
+err:
+ if (nv3031b) {
+ if (panel_dev_config->reset_gpio_num >= 0) {
+ gpio_reset_pin(panel_dev_config->reset_gpio_num);
+ }
+ free(nv3031b);
+ }
+ return ret;
+}
+
+static esp_err_t tx_param(esp_lcd_panel_io_handle_t io, int lcd_cmd, const void *param, size_t param_size)
+{
+ lcd_cmd &= 0xff;
+ lcd_cmd <<= 8;
+ lcd_cmd |= LCD_OPCODE_WRITE_CMD << 24;
+ return esp_lcd_panel_io_tx_param(io, lcd_cmd, param, param_size);
+}
+
+static esp_err_t tx_color(esp_lcd_panel_io_handle_t io, int lcd_cmd, const void *param, size_t param_size)
+{
+ lcd_cmd &= 0xff;
+ lcd_cmd <<= 8;
+ lcd_cmd |= LCD_OPCODE_WRITE_COLOR << 24;
+ return esp_lcd_panel_io_tx_color(io, lcd_cmd, param, param_size);
+}
+
+static esp_err_t panel_nv3031b_del(esp_lcd_panel_t *panel)
+{
+ nv3031b_panel_t *nv3031b = __containerof(panel, nv3031b_panel_t, base);
+
+ if (nv3031b->reset_gpio_num >= 0) {
+ gpio_reset_pin(nv3031b->reset_gpio_num);
+ }
+ ESP_LOGD(TAG, "del nv3031b panel @%p", nv3031b);
+ free(nv3031b);
+ return ESP_OK;
+}
+
+static esp_err_t panel_nv3031b_reset(esp_lcd_panel_t *panel)
+{
+ nv3031b_panel_t *nv3031b = __containerof(panel, nv3031b_panel_t, base);
+ esp_lcd_panel_io_handle_t io = nv3031b->io;
+
+ if (nv3031b->reset_gpio_num >= 0) {
+ gpio_set_level(nv3031b->reset_gpio_num, nv3031b->flags.reset_level);
+ vTaskDelay(pdMS_TO_TICKS(10));
+ gpio_set_level(nv3031b->reset_gpio_num, !nv3031b->flags.reset_level);
+ vTaskDelay(pdMS_TO_TICKS(NV3031B_RESET_DELAY_MS));
+ } else {
+ ESP_RETURN_ON_ERROR(tx_param(io, LCD_CMD_SWRESET, NULL, 0), TAG, "send command failed");
+ vTaskDelay(pdMS_TO_TICKS(NV3031B_RESET_DELAY_MS));
+ }
+
+ return ESP_OK;
+}
+
+static esp_err_t panel_nv3031b_init(esp_lcd_panel_t *panel)
+{
+ nv3031b_panel_t *nv3031b = __containerof(panel, nv3031b_panel_t, base);
+ esp_lcd_panel_io_handle_t io = nv3031b->io;
+
+ // A NOP frame first resynchronizes the QSPI interface
+ ESP_RETURN_ON_ERROR(tx_param(io, LCD_CMD_NOP, NULL, 0), TAG, "send command failed");
+
+ for (int i = 0; i < nv3031b->init_cmds_size; i++) {
+ const nv3031b_lcd_init_cmd_t *cmd = &nv3031b->init_cmds[i];
+ ESP_RETURN_ON_ERROR(tx_param(io, cmd->cmd, cmd->data, cmd->data_bytes), TAG, "send command failed");
+ if (cmd->delay_ms > 0) {
+ vTaskDelay(pdMS_TO_TICKS(cmd->delay_ms));
+ }
+ }
+
+ ESP_RETURN_ON_ERROR(tx_param(io, LCD_CMD_COLMOD, (uint8_t[]) {
+ nv3031b->colmod_val,
+ }, 1), TAG, "send command failed");
+ ESP_RETURN_ON_ERROR(tx_param(io, LCD_CMD_SLPOUT, NULL, 0), TAG, "send command failed");
+ vTaskDelay(pdMS_TO_TICKS(NV3031B_SLEEP_OUT_DELAY_MS));
+ ESP_RETURN_ON_ERROR(tx_param(io, LCD_CMD_MADCTL, (uint8_t[]) {
+ nv3031b->madctl_val,
+ }, 1), TAG, "send command failed");
+
+ return ESP_OK;
+}
+
+static esp_err_t panel_nv3031b_draw_bitmap(esp_lcd_panel_t *panel, int x_start, int y_start, int x_end, int y_end, const void *color_data)
+{
+ nv3031b_panel_t *nv3031b = __containerof(panel, nv3031b_panel_t, base);
+ assert((x_start < x_end) && (y_start < y_end) && "start position must be smaller than end position");
+ esp_lcd_panel_io_handle_t io = nv3031b->io;
+
+ x_start += nv3031b->x_gap;
+ x_end += nv3031b->x_gap;
+ y_start += nv3031b->y_gap;
+ y_end += nv3031b->y_gap;
+
+ ESP_RETURN_ON_ERROR(tx_param(io, LCD_CMD_CASET, (uint8_t[]) {
+ (x_start >> 8) & 0xFF,
+ x_start & 0xFF,
+ ((x_end - 1) >> 8) & 0xFF,
+ (x_end - 1) & 0xFF,
+ }, 4), TAG, "send command failed");
+ ESP_RETURN_ON_ERROR(tx_param(io, LCD_CMD_RASET, (uint8_t[]) {
+ (y_start >> 8) & 0xFF,
+ y_start & 0xFF,
+ ((y_end - 1) >> 8) & 0xFF,
+ (y_end - 1) & 0xFF,
+ }, 4), TAG, "send command failed");
+
+ size_t len = (x_end - x_start) * (y_end - y_start) * nv3031b->fb_bits_per_pixel / 8;
+ ESP_RETURN_ON_ERROR(tx_color(io, LCD_CMD_RAMWR, color_data, len), TAG, "send color data failed");
+
+ return ESP_OK;
+}
+
+static esp_err_t panel_nv3031b_invert_color(esp_lcd_panel_t *panel, bool invert_color_data)
+{
+ nv3031b_panel_t *nv3031b = __containerof(panel, nv3031b_panel_t, base);
+ int command = invert_color_data ? LCD_CMD_INVON : LCD_CMD_INVOFF;
+ ESP_RETURN_ON_ERROR(tx_param(nv3031b->io, command, NULL, 0), TAG, "send command failed");
+ return ESP_OK;
+}
+
+static esp_err_t panel_nv3031b_mirror(esp_lcd_panel_t *panel, bool mirror_x, bool mirror_y)
+{
+ nv3031b_panel_t *nv3031b = __containerof(panel, nv3031b_panel_t, base);
+
+ if (mirror_x) {
+ nv3031b->madctl_val |= LCD_CMD_MX_BIT;
+ } else {
+ nv3031b->madctl_val &= ~LCD_CMD_MX_BIT;
+ }
+ if (mirror_y) {
+ nv3031b->madctl_val |= LCD_CMD_MY_BIT;
+ } else {
+ nv3031b->madctl_val &= ~LCD_CMD_MY_BIT;
+ }
+ ESP_RETURN_ON_ERROR(tx_param(nv3031b->io, LCD_CMD_MADCTL, (uint8_t[]) {
+ nv3031b->madctl_val
+ }, 1), TAG, "send command failed");
+ return ESP_OK;
+}
+
+static esp_err_t panel_nv3031b_swap_xy(esp_lcd_panel_t *panel, bool swap_axes)
+{
+ nv3031b_panel_t *nv3031b = __containerof(panel, nv3031b_panel_t, base);
+
+ if (swap_axes) {
+ nv3031b->madctl_val |= LCD_CMD_MV_BIT;
+ } else {
+ nv3031b->madctl_val &= ~LCD_CMD_MV_BIT;
+ }
+ ESP_RETURN_ON_ERROR(tx_param(nv3031b->io, LCD_CMD_MADCTL, (uint8_t[]) {
+ nv3031b->madctl_val
+ }, 1), TAG, "send command failed");
+ return ESP_OK;
+}
+
+static esp_err_t panel_nv3031b_set_gap(esp_lcd_panel_t *panel, int x_gap, int y_gap)
+{
+ nv3031b_panel_t *nv3031b = __containerof(panel, nv3031b_panel_t, base);
+ nv3031b->x_gap = x_gap;
+ nv3031b->y_gap = y_gap;
+ return ESP_OK;
+}
+
+static esp_err_t panel_nv3031b_disp_on_off(esp_lcd_panel_t *panel, bool on_off)
+{
+ nv3031b_panel_t *nv3031b = __containerof(panel, nv3031b_panel_t, base);
+
+ if (on_off) {
+ // DISPON needs one dummy parameter byte on this controller
+ ESP_RETURN_ON_ERROR(tx_param(nv3031b->io, LCD_CMD_DISPON, (uint8_t[]) {
+ 0x00
+ }, 1), TAG, "send command failed");
+ } else {
+ ESP_RETURN_ON_ERROR(tx_param(nv3031b->io, LCD_CMD_DISPOFF, NULL, 0), TAG, "send command failed");
+ }
+ return ESP_OK;
+}
diff --git a/Drivers/nv3031b-module/source/module.cpp b/Drivers/nv3031b-module/source/module.cpp
new file mode 100644
index 000000000..04ff866fc
--- /dev/null
+++ b/Drivers/nv3031b-module/source/module.cpp
@@ -0,0 +1,23 @@
+// SPDX-License-Identifier: Apache-2.0
+#include
+#include
+
+extern "C" {
+
+extern Driver nv3031b_driver;
+
+static Driver* const nv3031b_drivers[] = {
+ &nv3031b_driver,
+ nullptr
+};
+
+Module nv3031b_module = {
+ .name = "nv3031b",
+ .start = nullptr,
+ .stop = nullptr,
+ .drivers = nv3031b_drivers,
+ .symbols = nullptr,
+ .internal = nullptr
+};
+
+} // extern "C"
diff --git a/Drivers/nv3031b-module/source/nv3031b.cpp b/Drivers/nv3031b-module/source/nv3031b.cpp
new file mode 100644
index 000000000..7053364d3
--- /dev/null
+++ b/Drivers/nv3031b-module/source/nv3031b.cpp
@@ -0,0 +1,379 @@
+// SPDX-License-Identifier: Apache-2.0
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+#include
+
+constexpr auto* TAG = "NV3031B";
+#define GET_CONFIG(device) (static_cast((device)->config))
+
+struct Nv3031bInternal {
+ Device* spi_controller;
+ esp_lcd_panel_io_handle_t io_handle;
+ esp_lcd_panel_handle_t panel_handle;
+ // draw_bitmap() must block until the transfer physically completes
+ SemaphoreHandle_t draw_done_semaphore;
+};
+
+static bool IRAM_ATTR on_color_trans_done(esp_lcd_panel_io_handle_t, esp_lcd_panel_io_event_data_t*, void* user_ctx) {
+ auto* internal = static_cast(user_ctx);
+ BaseType_t high_task_woken = pdFALSE;
+ xSemaphoreGiveFromISR(internal->draw_done_semaphore, &high_task_woken);
+ return high_task_woken == pdTRUE;
+}
+
+static int pin_or_unused(const struct GpioPinSpec& pin) {
+ return pin.gpio_controller == nullptr ? -1 : static_cast(pin.pin);
+}
+
+// Drives physical levels because IO expanders reject ACTIVE_LOW on outputs
+static error_t hardware_reset(const struct GpioPinSpec& pin) {
+ if (pin.gpio_controller == nullptr) {
+ return ERROR_NONE;
+ }
+
+ auto* reset = gpio_descriptor_acquire(pin.gpio_controller, pin.pin, GPIO_FLAG_DIRECTION_OUTPUT, GPIO_OWNER_GPIO);
+ if (reset == nullptr) {
+ LOG_E(TAG, "Failed to acquire reset pin");
+ return ERROR_RESOURCE;
+ }
+
+ error_t error = gpio_descriptor_set_level(reset, false);
+ if (error == ERROR_NONE) {
+ delay_millis(10);
+ error = gpio_descriptor_set_level(reset, true);
+ }
+ gpio_descriptor_release(reset);
+ if (error != ERROR_NONE) {
+ LOG_E(TAG, "Failed to pulse reset pin");
+ return error;
+ }
+
+ delay_millis(120);
+ return ERROR_NONE;
+}
+
+// region Driver lifecycle
+
+static error_t start(Device* device) {
+ auto* parent = device_get_parent(device);
+ check(device_get_type(parent) == &SPI_CONTROLLER_TYPE);
+
+ const auto* spi_config = static_cast(parent->config);
+ const auto* config = GET_CONFIG(device);
+
+ struct GpioPinSpec cs_pin;
+ if (esp32_spi_get_cs_pin(device, &cs_pin) != ERROR_NONE) {
+ LOG_E(TAG, "Failed to resolve CS pin");
+ return ERROR_RESOURCE;
+ }
+
+ error_t error = hardware_reset(config->pin_reset);
+ if (error != ERROR_NONE) {
+ return error;
+ }
+
+ auto* internal = static_cast(malloc(sizeof(Nv3031bInternal)));
+ if (internal == nullptr) {
+ return ERROR_OUT_OF_MEMORY;
+ }
+
+ internal->spi_controller = parent;
+ internal->draw_done_semaphore = xSemaphoreCreateBinary();
+ if (internal->draw_done_semaphore == nullptr) {
+ free(internal);
+ return ERROR_OUT_OF_MEMORY;
+ }
+
+ esp_lcd_panel_io_spi_config_t io_config = {
+ .cs_gpio_num = static_cast(pin_or_unused(cs_pin)),
+ .dc_gpio_num = GPIO_NUM_NC,
+ .spi_mode = 3,
+ .pclk_hz = config->pixel_clock_hz,
+ .trans_queue_depth = config->transaction_queue_depth,
+ .on_color_trans_done = on_color_trans_done,
+ .user_ctx = internal,
+ .lcd_cmd_bits = 32,
+ .lcd_param_bits = 8,
+ .cs_ena_pretrans = 0,
+ .cs_ena_posttrans = 0,
+ .flags = {
+ .dc_high_on_cmd = 0,
+ .dc_low_on_data = 0,
+ .dc_low_on_param = 0,
+ .octal_mode = 0,
+ .quad_mode = 1,
+ .sio_mode = 0,
+ .psram_dma_direct = 0,
+ .lsb_first = 0,
+ .cs_high_active = 0,
+ },
+ };
+
+ esp_err_t ret = esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)spi_config->host, &io_config, &internal->io_handle);
+ if (ret != ESP_OK) {
+ LOG_E(TAG, "Failed to create panel IO: %s", esp_err_to_name(ret));
+ vSemaphoreDelete(internal->draw_done_semaphore);
+ free(internal);
+ return ERROR_RESOURCE;
+ }
+
+ nv3031b_vendor_config_t vendor_config = {
+ .init_cmds = nullptr,
+ .init_cmds_size = 0,
+ };
+
+ // The hardware reset is done above, so the panel only gets a software reset
+ esp_lcd_panel_dev_config_t panel_config = {
+ .rgb_ele_order = config->bgr_order ? LCD_RGB_ELEMENT_ORDER_BGR : LCD_RGB_ELEMENT_ORDER_RGB,
+ .data_endian = LCD_RGB_DATA_ENDIAN_LITTLE,
+ .bits_per_pixel = 16,
+ .reset_gpio_num = GPIO_NUM_NC,
+ .vendor_config = &vendor_config,
+ .flags = { .reset_active_high = false },
+ };
+
+ ret = esp_lcd_new_panel_nv3031b(internal->io_handle, &panel_config, &internal->panel_handle);
+ if (ret != ESP_OK) {
+ LOG_E(TAG, "Failed to create panel: %s", esp_err_to_name(ret));
+ esp_lcd_panel_io_del(internal->io_handle);
+ vSemaphoreDelete(internal->draw_done_semaphore);
+ free(internal);
+ return ERROR_RESOURCE;
+ }
+
+ // Every failure path below must clean up fully: start_device is not retried by the kernel
+ spi_controller_lock(internal->spi_controller);
+ bool ok =
+ esp_lcd_panel_reset(internal->panel_handle) == ESP_OK &&
+ esp_lcd_panel_init(internal->panel_handle) == ESP_OK;
+ ok = ok && ((config->gap_x == 0 && config->gap_y == 0) || esp_lcd_panel_set_gap(internal->panel_handle, config->gap_x, config->gap_y) == ESP_OK);
+ ok = ok && (!config->swap_xy || esp_lcd_panel_swap_xy(internal->panel_handle, true) == ESP_OK);
+ ok = ok && ((!config->mirror_x && !config->mirror_y) || esp_lcd_panel_mirror(internal->panel_handle, config->mirror_x, config->mirror_y) == ESP_OK);
+ ok = ok && esp_lcd_panel_invert_color(internal->panel_handle, config->invert_color) == ESP_OK;
+ ok = ok && esp_lcd_panel_disp_on_off(internal->panel_handle, true) == ESP_OK;
+ spi_controller_unlock(internal->spi_controller);
+
+ if (!ok) {
+ LOG_E(TAG, "Failed to bring up panel");
+ esp_lcd_panel_del(internal->panel_handle);
+ esp_lcd_panel_io_del(internal->io_handle);
+ vSemaphoreDelete(internal->draw_done_semaphore);
+ free(internal);
+ return ERROR_RESOURCE;
+ }
+
+ device_set_driver_data(device, internal);
+ return ERROR_NONE;
+}
+
+static error_t stop(Device* device) {
+ auto* internal = static_cast(device_get_driver_data(device));
+
+ spi_controller_lock(internal->spi_controller);
+ if (internal->panel_handle != nullptr) {
+ if (esp_lcd_panel_del(internal->panel_handle) != ESP_OK) {
+ LOG_E(TAG, "Failed to delete panel");
+ spi_controller_unlock(internal->spi_controller);
+ return ERROR_RESOURCE;
+ }
+ internal->panel_handle = nullptr;
+ }
+
+ if (internal->io_handle != nullptr) {
+ if (esp_lcd_panel_io_del(internal->io_handle) != ESP_OK) {
+ LOG_E(TAG, "Failed to delete panel IO");
+ spi_controller_unlock(internal->spi_controller);
+ return ERROR_RESOURCE;
+ }
+ internal->io_handle = nullptr;
+ }
+ spi_controller_unlock(internal->spi_controller);
+
+ vSemaphoreDelete(internal->draw_done_semaphore);
+ free(internal);
+ device_set_driver_data(device, nullptr);
+ return ERROR_NONE;
+}
+
+// endregion
+
+// region DisplayApi
+
+static error_t nv3031b_reset(Device* device) {
+ auto* internal = static_cast(device_get_driver_data(device));
+ spi_controller_lock(internal->spi_controller);
+ error_t result = esp_lcd_panel_reset(internal->panel_handle) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
+ spi_controller_unlock(internal->spi_controller);
+ return result;
+}
+
+static error_t nv3031b_init(Device* device) {
+ auto* internal = static_cast(device_get_driver_data(device));
+ spi_controller_lock(internal->spi_controller);
+ error_t result = esp_lcd_panel_init(internal->panel_handle) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
+ spi_controller_unlock(internal->spi_controller);
+ return result;
+}
+
+static error_t nv3031b_draw_bitmap(Device* device, int32_t x_start, int32_t y_start, int32_t x_end, int32_t y_end, const void* color_data) {
+ auto* internal = static_cast(device_get_driver_data(device));
+
+ xSemaphoreTake(internal->draw_done_semaphore, 0);
+
+ spi_controller_lock(internal->spi_controller);
+ esp_err_t ret = esp_lcd_panel_draw_bitmap(internal->panel_handle, x_start, y_start, x_end, y_end, color_data);
+ if (ret != ESP_OK) {
+ spi_controller_unlock(internal->spi_controller);
+ return ERROR_RESOURCE;
+ }
+
+ // The bus lock is held until the transfer is done so no other bus user can interleave
+ xSemaphoreTake(internal->draw_done_semaphore, portMAX_DELAY);
+ spi_controller_unlock(internal->spi_controller);
+ return ERROR_NONE;
+}
+
+static error_t nv3031b_mirror(Device* device, bool x_axis, bool y_axis) {
+ auto* internal = static_cast(device_get_driver_data(device));
+ spi_controller_lock(internal->spi_controller);
+ error_t result = esp_lcd_panel_mirror(internal->panel_handle, x_axis, y_axis) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
+ spi_controller_unlock(internal->spi_controller);
+ return result;
+}
+
+static error_t nv3031b_swap_xy(Device* device, bool swap_axes) {
+ auto* internal = static_cast(device_get_driver_data(device));
+ spi_controller_lock(internal->spi_controller);
+ error_t result = esp_lcd_panel_swap_xy(internal->panel_handle, swap_axes) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
+ spi_controller_unlock(internal->spi_controller);
+ return result;
+}
+
+static bool nv3031b_get_swap_xy(Device* device) {
+ return GET_CONFIG(device)->swap_xy;
+}
+
+static bool nv3031b_get_mirror_x(Device* device) {
+ return GET_CONFIG(device)->mirror_x;
+}
+
+static bool nv3031b_get_mirror_y(Device* device) {
+ return GET_CONFIG(device)->mirror_y;
+}
+
+static error_t nv3031b_set_gap(Device* device, int32_t x_gap, int32_t y_gap) {
+ auto* internal = static_cast(device_get_driver_data(device));
+ spi_controller_lock(internal->spi_controller);
+ error_t result = esp_lcd_panel_set_gap(internal->panel_handle, x_gap, y_gap) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
+ spi_controller_unlock(internal->spi_controller);
+ return result;
+}
+
+static int32_t nv3031b_get_gap_x(Device* device) {
+ return GET_CONFIG(device)->gap_x;
+}
+
+static int32_t nv3031b_get_gap_y(Device* device) {
+ return GET_CONFIG(device)->gap_y;
+}
+
+static error_t nv3031b_invert_color(Device* device, bool invert_color_data) {
+ auto* internal = static_cast(device_get_driver_data(device));
+ spi_controller_lock(internal->spi_controller);
+ error_t result = esp_lcd_panel_invert_color(internal->panel_handle, invert_color_data) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
+ spi_controller_unlock(internal->spi_controller);
+ return result;
+}
+
+static error_t nv3031b_disp_on_off(Device* device, bool on_off) {
+ auto* internal = static_cast(device_get_driver_data(device));
+ spi_controller_lock(internal->spi_controller);
+ error_t result = esp_lcd_panel_disp_on_off(internal->panel_handle, on_off) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
+ spi_controller_unlock(internal->spi_controller);
+ return result;
+}
+
+// Pixels go out big-endian on the bus
+static enum DisplayColorFormat nv3031b_get_color_format(Device*) {
+ return DISPLAY_COLOR_FORMAT_RGB565_SWAPPED;
+}
+
+static uint16_t nv3031b_get_resolution_x(Device* device) {
+ return GET_CONFIG(device)->horizontal_resolution;
+}
+
+static uint16_t nv3031b_get_resolution_y(Device* device) {
+ return GET_CONFIG(device)->vertical_resolution;
+}
+
+static error_t nv3031b_get_backlight(Device* device, Device** backlight) {
+ auto* configured_backlight = GET_CONFIG(device)->backlight;
+ if (configured_backlight == nullptr) {
+ return ERROR_NOT_SUPPORTED;
+ }
+ *backlight = configured_backlight;
+ return ERROR_NONE;
+}
+
+// endregion
+
+static const DisplayApi nv3031b_display_api = {
+ .capabilities = DISPLAY_CAPABILITY_CAP_MIRROR | DISPLAY_CAPABILITY_CAP_SWAP_XY |
+ DISPLAY_CAPABILITY_CAP_SET_GAP | DISPLAY_CAPABILITY_INVERT_COLOR | DISPLAY_CAPABILITY_ON_OFF |
+ DISPLAY_CAPABILITY_BACKLIGHT,
+ .reset = nv3031b_reset,
+ .init = nv3031b_init,
+ .draw_bitmap = nv3031b_draw_bitmap,
+ .clear = nullptr,
+ .refresh = nullptr,
+ .mirror = nv3031b_mirror,
+ .swap_xy = nv3031b_swap_xy,
+ .get_swap_xy = nv3031b_get_swap_xy,
+ .get_mirror_x = nv3031b_get_mirror_x,
+ .get_mirror_y = nv3031b_get_mirror_y,
+ .set_gap = nv3031b_set_gap,
+ .get_gap_x = nv3031b_get_gap_x,
+ .get_gap_y = nv3031b_get_gap_y,
+ .invert_color = nv3031b_invert_color,
+ .disp_on_off = nv3031b_disp_on_off,
+ .disp_sleep = nullptr,
+ .get_color_format = nv3031b_get_color_format,
+ .get_resolution_x = nv3031b_get_resolution_x,
+ .get_resolution_y = nv3031b_get_resolution_y,
+ .get_frame_buffer = nullptr,
+ .get_frame_buffer_count = nullptr,
+ .get_backlight = nv3031b_get_backlight,
+ .has_capability = nullptr,
+};
+
+Driver nv3031b_driver = {
+ .name = "nv3031b",
+ .compatible = (const char*[]) { "newvision,nv3031b", nullptr },
+ .start_device = start,
+ .stop_device = stop,
+ .probe = nullptr,
+ .api = &nv3031b_display_api,
+ .device_type = &DISPLAY_TYPE,
+ .owner = &nv3031b_module,
+ .internal = nullptr
+};
diff --git a/Drivers/nv3031b-module/source/nv3031b_init_cmds.h b/Drivers/nv3031b-module/source/nv3031b_init_cmds.h
new file mode 100644
index 000000000..14b9dc2c9
--- /dev/null
+++ b/Drivers/nv3031b-module/source/nv3031b_init_cmds.h
@@ -0,0 +1,34 @@
+/*
+ * SPDX-FileCopyrightText: 2020 lovyan03 (https://github.com/lovyan03)
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Initialization sequence from LovyanGFX src/lgfx/v1/panel/Panel_NV3031B.hpp
+ * https://github.com/lovyan03/LovyanGFX
+ */
+#pragma once
+
+#include "esp_lcd_nv3031b.h"
+
+static const nv3031b_lcd_init_cmd_t nv3031b_init_cmds_default[] = {
+ {0xFD, (uint8_t[]) {0x06, 0x08}, 2, 0},
+ {0x60, (uint8_t[]) {0x0C}, 1, 0},
+ {0x61, (uint8_t[]) {0x07, 0x04}, 2, 0},
+ {0xB4, (uint8_t[]) {0x01}, 1, 0},
+ {0xB1, (uint8_t[]) {0x0F, 0x02, 0x03}, 3, 0},
+ {0xB5, (uint8_t[]) {0x02, 0x02, 0x0A, 0x14}, 4, 0},
+ {0xB6, (uint8_t[]) {0x44, 0x01, 0x9F, 0x00, 0x02}, 5, 0},
+ {0xDF, (uint8_t[]) {0x11}, 1, 0},
+ {0x67, (uint8_t[]) {0x21}, 1, 0},
+ {0x68, (uint8_t[]) {0x90, 0x4F, 0x27, 0x21}, 4, 0},
+ {0xE1, (uint8_t[]) {0x20, 0x69}, 2, 0},
+ {0xE4, (uint8_t[]) {0x69, 0x20}, 2, 0},
+ {0xE2, (uint8_t[]) {0x10, 0x12, 0x12, 0x30, 0x39, 0x3F}, 6, 0},
+ {0xE5, (uint8_t[]) {0x3F, 0x33, 0x2D, 0x12, 0x12, 0x10}, 6, 0},
+ {0xE0, (uint8_t[]) {0x06, 0x06, 0x0B, 0x12, 0x11, 0x11, 0x0E, 0x19}, 8, 0},
+ {0xE3, (uint8_t[]) {0x19, 0x13, 0x14, 0x14, 0x14, 0x12, 0x08, 0x05}, 8, 0},
+ {0xE6, (uint8_t[]) {0x00, 0xFF}, 2, 0},
+ {0xE7, (uint8_t[]) {0x01, 0x04, 0x03, 0x03, 0x00, 0x12}, 6, 0},
+ {0xE8, (uint8_t[]) {0x00, 0x70, 0x00}, 3, 0},
+ {0xEC, (uint8_t[]) {0x54}, 1, 0},
+ {0xFD, (uint8_t[]) {0xFA, 0xFC}, 2, 0},
+};
diff --git a/THIRD-PARTY-NOTICES.md b/THIRD-PARTY-NOTICES.md
index 7696d9070..1c3e36e16 100644
--- a/THIRD-PARTY-NOTICES.md
+++ b/THIRD-PARTY-NOTICES.md
@@ -24,6 +24,14 @@ Website: https://github.com/vroland/epdiy
License: [LGPL v3.0 or later](https://github.com/vroland/epdiy/blob/main/LICENSE)
+### esp-iot-solution
+
+The NV3031B panel driver (`Drivers/nv3031b-module`) is adapted from the `esp_lcd_sh8601` component.
+
+Website: https://github.com/espressif/esp-iot-solution
+
+License: [Apache License v2.0](https://github.com/espressif/esp-iot-solution/blob/master/LICENSE)
+
### ESP-IDF
This project uses ESP-IDF to compile the ESP32 firmware.
@@ -53,6 +61,14 @@ Website: https://github.com/nayarsystems/posix_tz_db
License: [MIT](https://github.com/nayarsystems/posix_tz_db/blob/master/LICENSE)
+### LovyanGFX
+
+The NV3031B initialization sequence in `Drivers/nv3031b-module/source/nv3031b_init_cmds.h` is taken from LovyanGFX.
+
+Website: https://github.com/lovyan03/LovyanGFX
+
+License: [FreeBSD (BSD 2-Clause)](https://github.com/lovyan03/LovyanGFX/blob/master/license.txt)
+
### Minmea
Website: https://github.com/kosma/minmea
From 18f94a829e0872020ceedd6359e4deb4e2b0e64c Mon Sep 17 00:00:00 2001
From: AdaSzi115026 <81822538+AdaSzi@users.noreply.github.com>
Date: Mon, 5 Oct 2026 23:11:05 +0200
Subject: [PATCH 3/5] Add battery, audio, LoRa, Grove and buttons to the Wio
Tracker L2 Pro
New driver modules:
- ads1115-module: TI ADS1115 ADC, written from the datasheet. The device uses it with battery-sense for the battery voltage.
- es7243e-module: ES7243E microphone ADC, wraps the esp_codec_dev implementation.
Device additions: ES8311 speaker and ES7243E microphone on I2S, battery voltage, SX1262 LoRa radio, Grove port, WAKE and BOOT buttons as navigation keys.
Tested on hardware: speaker and microphone (audio recorder app), battery voltage and charge level, both buttons, SX1262 probe. Not tested: LoRa transmit and receive, Grove port.
---
Devices/seeed-wio-tracker-l2-pro/module.yaml | 6 +
.../seeed,wio-tracker-l2-pro.dts | 86 ++++-
Drivers/ads1115-module/CMakeLists.txt | 11 +
Drivers/ads1115-module/LICENSE-Apache-2.0.md | 195 +++++++++++
Drivers/ads1115-module/README.md | 10 +
.../ads1115-module/bindings/ti,ads1115.yaml | 15 +
.../ads1115-module/include/ads1115_module.h | 14 +
.../ads1115-module/include/bindings/ads1115.h | 7 +
.../ads1115-module/include/drivers/ads1115.h | 17 +
Drivers/ads1115-module/module.yaml | 3 +
Drivers/ads1115-module/source/ads1115.cpp | 138 ++++++++
Drivers/ads1115-module/source/module.cpp | 23 ++
Drivers/es7243e-module/CMakeLists.txt | 11 +
Drivers/es7243e-module/LICENSE-Apache-2.0.md | 195 +++++++++++
Drivers/es7243e-module/README.md | 10 +
.../bindings/everest,es7243e.yaml | 15 +
.../es7243e-module/include/bindings/es7243e.h | 15 +
.../es7243e-module/include/drivers/es7243e.h | 23 ++
.../es7243e-module/include/es7243e_module.h | 14 +
Drivers/es7243e-module/module.yaml | 3 +
Drivers/es7243e-module/source/es7243e.cpp | 328 ++++++++++++++++++
Drivers/es7243e-module/source/module.cpp | 23 ++
22 files changed, 1161 insertions(+), 1 deletion(-)
create mode 100644 Drivers/ads1115-module/CMakeLists.txt
create mode 100644 Drivers/ads1115-module/LICENSE-Apache-2.0.md
create mode 100644 Drivers/ads1115-module/README.md
create mode 100644 Drivers/ads1115-module/bindings/ti,ads1115.yaml
create mode 100644 Drivers/ads1115-module/include/ads1115_module.h
create mode 100644 Drivers/ads1115-module/include/bindings/ads1115.h
create mode 100644 Drivers/ads1115-module/include/drivers/ads1115.h
create mode 100644 Drivers/ads1115-module/module.yaml
create mode 100644 Drivers/ads1115-module/source/ads1115.cpp
create mode 100644 Drivers/ads1115-module/source/module.cpp
create mode 100644 Drivers/es7243e-module/CMakeLists.txt
create mode 100644 Drivers/es7243e-module/LICENSE-Apache-2.0.md
create mode 100644 Drivers/es7243e-module/README.md
create mode 100644 Drivers/es7243e-module/bindings/everest,es7243e.yaml
create mode 100644 Drivers/es7243e-module/include/bindings/es7243e.h
create mode 100644 Drivers/es7243e-module/include/drivers/es7243e.h
create mode 100644 Drivers/es7243e-module/include/es7243e_module.h
create mode 100644 Drivers/es7243e-module/module.yaml
create mode 100644 Drivers/es7243e-module/source/es7243e.cpp
create mode 100644 Drivers/es7243e-module/source/module.cpp
diff --git a/Devices/seeed-wio-tracker-l2-pro/module.yaml b/Devices/seeed-wio-tracker-l2-pro/module.yaml
index c6d1e683e..4670e0545 100644
--- a/Devices/seeed-wio-tracker-l2-pro/module.yaml
+++ b/Devices/seeed-wio-tracker-l2-pro/module.yaml
@@ -1,6 +1,12 @@
dependencies:
- Platforms/platform-esp32
- Drivers/tca95xx-16bit-module
+ - Drivers/ads1115-module
+ - Drivers/button-control-module
+ - Drivers/es7243e-module
+ - Drivers/es8311-module
+ - Drivers/audio-stream-module
+ - Drivers/sx126x-module
- Drivers/gt911-module
- Drivers/lp5814-module
- Drivers/nv3031b-module
diff --git a/Devices/seeed-wio-tracker-l2-pro/seeed,wio-tracker-l2-pro.dts b/Devices/seeed-wio-tracker-l2-pro/seeed,wio-tracker-l2-pro.dts
index 1399af3a3..ac7205df8 100644
--- a/Devices/seeed-wio-tracker-l2-pro/seeed,wio-tracker-l2-pro.dts
+++ b/Devices/seeed-wio-tracker-l2-pro/seeed,wio-tracker-l2-pro.dts
@@ -1,9 +1,12 @@
/dts-v1/;
#include
+#include
#include
#include
+#include
#include
+#include
#include
#include
#include
@@ -11,9 +14,14 @@
#include
#include
#include
+#include
+#include
+#include
+#include
#include
#include
#include
+#include
#include
// Reference: https://wiki.seeedstudio.com/meshtastic_wio_tracker_l2_intro/
@@ -34,6 +42,17 @@
gpio-count = <49>;
};
+ // Declared before i2c0 so the codec can reference it
+ i2s0 {
+ compatible = "espressif,esp32-i2s";
+ port = ;
+ pin-mclk = <&gpio0 10 GPIO_FLAG_NONE>;
+ pin-bclk = <&gpio0 11 GPIO_FLAG_NONE>;
+ pin-ws = <&gpio0 12 GPIO_FLAG_NONE>;
+ pin-data-out = <&gpio0 16 GPIO_FLAG_NONE>;
+ pin-data-in = <&gpio0 15 GPIO_FLAG_NONE>;
+ };
+
i2c0 {
compatible = "espressif,esp32-i2c-master";
port = ;
@@ -80,7 +99,7 @@
speaker_amplifier {
compatible = "gpio-hog";
pin = <&pca9555 12 GPIO_FLAG_NONE>;
- mode = ;
+ mode = ;
};
gnss_power {
@@ -114,6 +133,27 @@
pin-interrupt = <&pca9555 3 GPIO_FLAG_NONE>;
};
+ // Speaker
+ es8311 {
+ compatible = "everest,es8311";
+ reg = <0x18>;
+ i2s = <&i2s0>;
+ };
+
+ // Microphone
+ es7243e {
+ compatible = "everest,es7243e";
+ reg = <0x14>;
+ i2s = <&i2s0>;
+ };
+
+ // Battery on AIN0 through a 1:2 divider that is enabled by expander pin 15
+ ads1115 {
+ compatible = "ti,ads1115";
+ reg = <0x48>;
+ full-scale-mv = <4096>;
+ };
+
// Started at boot after the display is drawn
display_backlight {
compatible = "ti,lp5814";
@@ -124,6 +164,39 @@
};
};
+ battery-sense {
+ compatible = "battery-sense";
+ io-channel = <&ads1115 0>;
+ reference-voltage-mv = <4096>;
+ multiplier = <2000>;
+ };
+
+ // WAKE: short press enter, long press back. BOOT: short press next, long press previous.
+ buttons {
+ compatible = "tactility,button-control";
+ pin-primary = <&pca9555 0 GPIO_FLAG_NONE>;
+ pin-secondary = <&gpio0 0 GPIO_FLAG_PULL_UP>;
+ };
+
+ spi0 {
+ compatible = "espressif,esp32-spi";
+ host = ;
+ cs-gpios = <&gpio0 21 GPIO_FLAG_NONE>;
+ pin-sclk = <&gpio0 4 GPIO_FLAG_NONE>;
+ pin-mosi = <&gpio0 6 GPIO_FLAG_NONE>;
+ pin-miso = <&gpio0 5 GPIO_FLAG_NONE>;
+
+ radio@0 {
+ compatible = "semtech,sx1262";
+ reg = <0>;
+ pin-reset = <&gpio0 7 GPIO_FLAG_NONE>;
+ pin-busy = <&gpio0 8 GPIO_FLAG_NONE>;
+ pin-dio1 = <&gpio0 9 GPIO_FLAG_NONE>;
+ tcxo-millivolts = <3000>;
+ dio2-as-rf-switch;
+ };
+ };
+
spi1 {
compatible = "espressif,esp32-spi";
host = ;
@@ -168,6 +241,17 @@
};
};
+ // Powered by expander pin 7
+ grove0 {
+ compatible = "espressif,esp32-grove";
+ defaultMode = ;
+ pinSdaTx = <&gpio0 13 GPIO_FLAG_NONE>;
+ pinSclRx = <&gpio0 14 GPIO_FLAG_NONE>;
+ uartPort = ;
+ i2cPort = ;
+ i2cClockFrequency = <400000>;
+ };
+
usbdevice0 {
compatible = "espressif,esp32-usbdevice";
diff --git a/Drivers/ads1115-module/CMakeLists.txt b/Drivers/ads1115-module/CMakeLists.txt
new file mode 100644
index 000000000..518c768d4
--- /dev/null
+++ b/Drivers/ads1115-module/CMakeLists.txt
@@ -0,0 +1,11 @@
+cmake_minimum_required(VERSION 3.20)
+
+include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake")
+
+file(GLOB_RECURSE SOURCE_FILES "source/*.c*")
+
+tactility_add_module(ads1115-module
+ SRCS ${SOURCE_FILES}
+ INCLUDE_DIRS include/
+ REQUIRES TactilityKernel
+)
diff --git a/Drivers/ads1115-module/LICENSE-Apache-2.0.md b/Drivers/ads1115-module/LICENSE-Apache-2.0.md
new file mode 100644
index 000000000..f5f4b8b5e
--- /dev/null
+++ b/Drivers/ads1115-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/ads1115-module/README.md b/Drivers/ads1115-module/README.md
new file mode 100644
index 000000000..f9a171c4c
--- /dev/null
+++ b/Drivers/ads1115-module/README.md
@@ -0,0 +1,10 @@
+# ADS1115 ADC
+
+A driver for the Texas Instruments `ADS1115` 16-bit I2C ADC, exposed as an ADC controller
+with the four single-ended inputs AIN0 to AIN3 as channels 0 to 3.
+
+Each read is a single-shot conversion. The result is returned as a 12-bit value: 0 to 4095
+covers 0 V to the configured full-scale voltage, negative readings are returned as 0.
+Written from the TI datasheet (SBAS444).
+
+License: [Apache v2.0](LICENSE-Apache-2.0.md)
diff --git a/Drivers/ads1115-module/bindings/ti,ads1115.yaml b/Drivers/ads1115-module/bindings/ti,ads1115.yaml
new file mode 100644
index 000000000..6fc8cd0d0
--- /dev/null
+++ b/Drivers/ads1115-module/bindings/ti,ads1115.yaml
@@ -0,0 +1,15 @@
+description: >
+ Texas Instruments ADS1115 16-bit ADC. Channels 0 to 3 are the single-ended inputs AIN0 to
+ AIN3. Results are returned as 12-bit values, 0 to 4095 for 0 V to full-scale-mv.
+
+include: ["i2c-device.yaml"]
+
+compatible: "ti,ads1115"
+
+bus: i2c
+
+properties:
+ full-scale-mv:
+ type: int
+ default: 4096
+ description: Programmable gain amplifier full scale in millivolts (6144, 4096, 2048, 1024, 512 or 256)
diff --git a/Drivers/ads1115-module/include/ads1115_module.h b/Drivers/ads1115-module/include/ads1115_module.h
new file mode 100644
index 000000000..44aec5a64
--- /dev/null
+++ b/Drivers/ads1115-module/include/ads1115_module.h
@@ -0,0 +1,14 @@
+// SPDX-License-Identifier: Apache-2.0
+#pragma once
+
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+extern struct Module ads1115_module;
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/Drivers/ads1115-module/include/bindings/ads1115.h b/Drivers/ads1115-module/include/bindings/ads1115.h
new file mode 100644
index 000000000..02a1a6dec
--- /dev/null
+++ b/Drivers/ads1115-module/include/bindings/ads1115.h
@@ -0,0 +1,7 @@
+// SPDX-License-Identifier: Apache-2.0
+#pragma once
+
+#include
+#include
+
+DEFINE_DEVICETREE(ads1115, struct Ads1115Config)
diff --git a/Drivers/ads1115-module/include/drivers/ads1115.h b/Drivers/ads1115-module/include/drivers/ads1115.h
new file mode 100644
index 000000000..9ef4ed4fc
--- /dev/null
+++ b/Drivers/ads1115-module/include/drivers/ads1115.h
@@ -0,0 +1,17 @@
+// SPDX-License-Identifier: Apache-2.0
+#pragma once
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include
+
+struct Ads1115Config {
+ uint8_t address;
+ uint16_t full_scale_mv;
+};
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/Drivers/ads1115-module/module.yaml b/Drivers/ads1115-module/module.yaml
new file mode 100644
index 000000000..a07d6f334
--- /dev/null
+++ b/Drivers/ads1115-module/module.yaml
@@ -0,0 +1,3 @@
+dependencies:
+ - TactilityKernel
+bindings: bindings
diff --git a/Drivers/ads1115-module/source/ads1115.cpp b/Drivers/ads1115-module/source/ads1115.cpp
new file mode 100644
index 000000000..9d113b36a
--- /dev/null
+++ b/Drivers/ads1115-module/source/ads1115.cpp
@@ -0,0 +1,138 @@
+// SPDX-License-Identifier: Apache-2.0
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#define TAG "ADS1115"
+#define GET_CONFIG(device) (static_cast((device)->config))
+
+static constexpr uint8_t REG_CONVERSION = 0x00;
+static constexpr uint8_t REG_CONFIG = 0x01;
+
+static constexpr uint16_t CONFIG_OS = 1U << 15;
+static constexpr uint16_t CONFIG_MUX_SINGLE_AIN0 = 0b100U << 12;
+static constexpr uint16_t CONFIG_PGA_SHIFT = 9;
+static constexpr uint16_t CONFIG_MODE_SINGLE_SHOT = 1U << 8;
+static constexpr uint16_t CONFIG_DR_860_SPS = 0b111U << 5;
+static constexpr uint16_t CONFIG_COMP_QUE_DISABLE = 0b11U;
+
+static constexpr uint8_t CHANNEL_COUNT = 4;
+// A conversion at 860 SPS takes about 1.2 ms
+static constexpr int CONVERSION_POLL_LIMIT = 10;
+
+extern "C" {
+
+static bool pga_bits_for(uint16_t full_scale_mv, uint16_t* out_bits) {
+ static constexpr uint16_t FULL_SCALES_MV[] = { 6144, 4096, 2048, 1024, 512, 256 };
+ for (uint16_t i = 0; i < sizeof(FULL_SCALES_MV) / sizeof(FULL_SCALES_MV[0]); i++) {
+ if (FULL_SCALES_MV[i] == full_scale_mv) {
+ *out_bits = i;
+ return true;
+ }
+ }
+ return false;
+}
+
+static error_t convert(Device* device, uint8_t channel, int16_t* out_code, TickType_t timeout) {
+ const auto* config = GET_CONFIG(device);
+ auto* i2c = device_get_parent(device);
+
+ uint16_t pga_bits = 0;
+ check(pga_bits_for(config->full_scale_mv, &pga_bits));
+
+ uint16_t start = CONFIG_OS | (CONFIG_MUX_SINGLE_AIN0 + (static_cast(channel) << 12)) | (pga_bits << CONFIG_PGA_SHIFT) |
+ CONFIG_MODE_SINGLE_SHOT | CONFIG_DR_860_SPS | CONFIG_COMP_QUE_DISABLE;
+ error_t error = i2c_controller_register16be_set(i2c, config->address, REG_CONFIG, start, timeout);
+ if (error != ERROR_NONE) {
+ return error;
+ }
+
+ // OS reads back as 1 once the conversion is done
+ for (int i = 0; i < CONVERSION_POLL_LIMIT; i++) {
+ delay_millis(1);
+ uint16_t status;
+ error = i2c_controller_register16be_get(i2c, config->address, REG_CONFIG, &status, timeout);
+ if (error != ERROR_NONE) {
+ return error;
+ }
+ if ((status & CONFIG_OS) != 0) {
+ uint16_t code;
+ error = i2c_controller_register16be_get(i2c, config->address, REG_CONVERSION, &code, timeout);
+ if (error == ERROR_NONE) {
+ *out_code = static_cast(code);
+ }
+ return error;
+ }
+ }
+
+ return ERROR_TIMEOUT;
+}
+
+static error_t read_raw(Device* device, uint8_t channel, int* out_raw, TickType_t timeout) {
+ if (channel >= CHANNEL_COUNT) {
+ return ERROR_OUT_OF_RANGE;
+ }
+
+ // A conversion is a sequence of transfers that must not interleave with another read
+ device_lock(device);
+ int16_t code = 0;
+ error_t error = convert(device, channel, &code, timeout);
+ device_unlock(device);
+
+ if (error != ERROR_NONE) {
+ return error;
+ }
+
+ // 15 bits of positive range scaled to 12 bits
+ *out_raw = code < 0 ? 0 : code >> 3;
+ return ERROR_NONE;
+}
+
+static constexpr AdcControllerApi ADS1115_API = {
+ .read_raw = read_raw
+};
+
+static error_t start(Device* device) {
+ check(device_get_type(device_get_parent(device)) == &I2C_CONTROLLER_TYPE);
+ const auto* config = GET_CONFIG(device);
+
+ uint16_t pga_bits;
+ if (!pga_bits_for(config->full_scale_mv, &pga_bits)) {
+ LOG_E(TAG, "Unsupported full-scale-mv %u", config->full_scale_mv);
+ return ERROR_INVALID_ARGUMENT;
+ }
+
+ uint16_t value;
+ error_t error = i2c_controller_register16be_get(device_get_parent(device), config->address, REG_CONFIG, &value, pdMS_TO_TICKS(50));
+ if (error != ERROR_NONE) {
+ LOG_E(TAG, "Not responding at 0x%02X", config->address);
+ return error;
+ }
+
+ return ERROR_NONE;
+}
+
+static error_t stop(Device*) {
+ return ERROR_NONE;
+}
+
+Driver ads1115_driver = {
+ .name = "ads1115",
+ .compatible = (const char*[]) { "ti,ads1115", nullptr },
+ .start_device = start,
+ .stop_device = stop,
+ .probe = nullptr,
+ .api = &ADS1115_API,
+ .device_type = &ADC_CONTROLLER_TYPE,
+ .owner = &ads1115_module,
+ .internal = nullptr
+};
+
+}
diff --git a/Drivers/ads1115-module/source/module.cpp b/Drivers/ads1115-module/source/module.cpp
new file mode 100644
index 000000000..38d37cacf
--- /dev/null
+++ b/Drivers/ads1115-module/source/module.cpp
@@ -0,0 +1,23 @@
+// SPDX-License-Identifier: Apache-2.0
+#include
+#include
+
+extern "C" {
+
+extern Driver ads1115_driver;
+
+static Driver* const ads1115_drivers[] = {
+ &ads1115_driver,
+ nullptr
+};
+
+Module ads1115_module = {
+ .name = "ads1115",
+ .start = nullptr,
+ .stop = nullptr,
+ .drivers = ads1115_drivers,
+ .symbols = nullptr,
+ .internal = nullptr
+};
+
+} // extern "C"
diff --git a/Drivers/es7243e-module/CMakeLists.txt b/Drivers/es7243e-module/CMakeLists.txt
new file mode 100644
index 000000000..fad688636
--- /dev/null
+++ b/Drivers/es7243e-module/CMakeLists.txt
@@ -0,0 +1,11 @@
+cmake_minimum_required(VERSION 3.20)
+
+include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake")
+
+file(GLOB_RECURSE SOURCE_FILES "source/*.c*")
+
+tactility_add_module(es7243e-module
+ SRCS ${SOURCE_FILES}
+ INCLUDE_DIRS include/
+ REQUIRES TactilityKernel audio-codec-module esp_codec_dev
+)
diff --git a/Drivers/es7243e-module/LICENSE-Apache-2.0.md b/Drivers/es7243e-module/LICENSE-Apache-2.0.md
new file mode 100644
index 000000000..f5f4b8b5e
--- /dev/null
+++ b/Drivers/es7243e-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/es7243e-module/README.md b/Drivers/es7243e-module/README.md
new file mode 100644
index 000000000..a204ee3f9
--- /dev/null
+++ b/Drivers/es7243e-module/README.md
@@ -0,0 +1,10 @@
+# ES7243E microphone ADC
+
+A driver for the `ES7243E` stereo audio ADC by Everest Semiconductor, wired as an
+input-only `AUDIO_CODEC_TYPE` device. The I2C bus is the device's parent, the I2S
+controller carrying the audio data is referenced via a devicetree phandle. The ADC
+runs as I2S slave with MCLK from the pad (MCLK = 256 x sample rate).
+
+Wraps Espressif's `esp_codec_dev` ES7243E implementation.
+
+License: [Apache v2.0](LICENSE-Apache-2.0.md)
diff --git a/Drivers/es7243e-module/bindings/everest,es7243e.yaml b/Drivers/es7243e-module/bindings/everest,es7243e.yaml
new file mode 100644
index 000000000..88bc618ae
--- /dev/null
+++ b/Drivers/es7243e-module/bindings/everest,es7243e.yaml
@@ -0,0 +1,15 @@
+description: Everest Semiconductor ES7243E stereo microphone ADC
+
+include: ["i2c-device.yaml"]
+
+compatible: "everest,es7243e"
+
+properties:
+ i2s:
+ type: phandle
+ required: true
+ description: "I2S controller device that carries audio data"
+ input-gain-percent:
+ type: int
+ default: 100
+ description: "Extra digital gain multiplier applied by audio_stream on top of the ADC's hardware gain, as an integer percentage (100 = 1.0x)"
diff --git a/Drivers/es7243e-module/include/bindings/es7243e.h b/Drivers/es7243e-module/include/bindings/es7243e.h
new file mode 100644
index 000000000..a0f3b196c
--- /dev/null
+++ b/Drivers/es7243e-module/include/bindings/es7243e.h
@@ -0,0 +1,15 @@
+// SPDX-License-Identifier: Apache-2.0
+#pragma once
+
+#include
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+DEFINE_DEVICETREE(es7243e, struct Es7243eConfig)
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/Drivers/es7243e-module/include/drivers/es7243e.h b/Drivers/es7243e-module/include/drivers/es7243e.h
new file mode 100644
index 000000000..60650d834
--- /dev/null
+++ b/Drivers/es7243e-module/include/drivers/es7243e.h
@@ -0,0 +1,23 @@
+// SPDX-License-Identifier: Apache-2.0
+#pragma once
+
+#include
+
+struct Device;
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct Es7243eConfig {
+ /** I2C address on the bus */
+ uint8_t address;
+ /** I2S controller device that carries audio data */
+ struct Device* i2s_device;
+ /** Extra digital gain applied by audio_stream as an integer percentage (100 = 1.0x) */
+ uint16_t input_gain_percent;
+};
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/Drivers/es7243e-module/include/es7243e_module.h b/Drivers/es7243e-module/include/es7243e_module.h
new file mode 100644
index 000000000..4c308f2e1
--- /dev/null
+++ b/Drivers/es7243e-module/include/es7243e_module.h
@@ -0,0 +1,14 @@
+// SPDX-License-Identifier: Apache-2.0
+#pragma once
+
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+extern struct Module es7243e_module;
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/Drivers/es7243e-module/module.yaml b/Drivers/es7243e-module/module.yaml
new file mode 100644
index 000000000..a07d6f334
--- /dev/null
+++ b/Drivers/es7243e-module/module.yaml
@@ -0,0 +1,3 @@
+dependencies:
+ - TactilityKernel
+bindings: bindings
diff --git a/Drivers/es7243e-module/source/es7243e.cpp b/Drivers/es7243e-module/source/es7243e.cpp
new file mode 100644
index 000000000..3d7ccf8ba
--- /dev/null
+++ b/Drivers/es7243e-module/source/es7243e.cpp
@@ -0,0 +1,328 @@
+// SPDX-License-Identifier: Apache-2.0
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+
+#define TAG "ES7243E"
+
+namespace {
+
+constexpr uint32_t NATIVE_SAMPLE_RATE = 16000;
+constexpr uint8_t NATIVE_CHANNELS = 2;
+constexpr float MAX_INPUT_GAIN_DB = 37.5f;
+
+struct Es7243eData {
+ const audio_codec_ctrl_if_t* ctrl_if = nullptr;
+ const audio_codec_data_if_t* data_if = nullptr;
+ const audio_codec_if_t* codec_if = nullptr;
+ esp_codec_dev_handle_t codec_device = nullptr;
+ bool is_open = false;
+ uint8_t open_bits_per_sample = 16;
+ uint8_t open_channels = NATIVE_CHANNELS;
+ uint32_t open_sample_rate = 0;
+ float input_gain = 1.0f;
+};
+
+#define GET_CONFIG(device) (static_cast((device)->config))
+#define GET_DATA(device) (static_cast(device_get_driver_data(device)))
+
+// region AudioCodecApi
+
+error_t open(Device* device, const struct AudioCodecStreamConfig* config) {
+ auto* data = GET_DATA(device);
+ if (data->codec_device == nullptr) {
+ return ERROR_RESOURCE;
+ }
+
+ if (config->direction != AUDIO_CODEC_DIR_INPUT) {
+ LOG_E(TAG, "ES7243E is input-only");
+ return ERROR_NOT_SUPPORTED;
+ }
+
+ uint8_t channels = (config->channels == 1) ? 1 : NATIVE_CHANNELS;
+
+ if (data->is_open) {
+ bool same_config = config->bits_per_sample == data->open_bits_per_sample
+ && channels == data->open_channels
+ && config->sample_rate == data->open_sample_rate;
+ return same_config ? ERROR_NONE : ERROR_RESOURCE;
+ }
+
+ esp_codec_dev_sample_info_t sample_info = {
+ .bits_per_sample = config->bits_per_sample,
+ .channel = channels,
+ .channel_mask = 0,
+ .sample_rate = config->sample_rate,
+ .mclk_multiple = 0,
+ };
+
+ if (esp_codec_dev_open(data->codec_device, &sample_info) != ESP_CODEC_DEV_OK) {
+ LOG_E(TAG, "Failed to open codec device");
+ return ERROR_RESOURCE;
+ }
+
+ data->open_bits_per_sample = config->bits_per_sample;
+ data->open_channels = channels;
+ data->open_sample_rate = config->sample_rate;
+ data->is_open = true;
+ return ERROR_NONE;
+}
+
+error_t close(Device* device) {
+ auto* data = GET_DATA(device);
+ if (data->codec_device == nullptr) {
+ return ERROR_RESOURCE;
+ }
+
+ if (data->is_open) {
+ esp_codec_dev_close(data->codec_device);
+ data->is_open = false;
+ }
+
+ return ERROR_NONE;
+}
+
+error_t read(Device* device, void* buffer, size_t size, size_t* bytes_read, TickType_t timeout) {
+ (void) timeout;
+ auto* data = GET_DATA(device);
+ if (!data->is_open) {
+ return ERROR_RESOURCE;
+ }
+
+ int result = esp_codec_dev_read(data->codec_device, buffer, (int) size);
+ if (result < 0) {
+ return ERROR_RESOURCE;
+ }
+ *bytes_read = (size_t) result;
+ return ERROR_NONE;
+}
+
+error_t write(Device* device, const void* buffer, size_t size, size_t* bytes_written, TickType_t timeout) {
+ (void) device;
+ (void) buffer;
+ (void) size;
+ (void) bytes_written;
+ (void) timeout;
+ return ERROR_NOT_SUPPORTED;
+}
+
+error_t set_volume(Device* device, AudioCodecDirection direction, float volume_percent) {
+ auto* data = GET_DATA(device);
+ if (data->codec_device == nullptr || direction != AUDIO_CODEC_DIR_INPUT) {
+ return ERROR_NOT_SUPPORTED;
+ }
+
+ float db = (volume_percent / 100.0f) * MAX_INPUT_GAIN_DB;
+ return (esp_codec_dev_set_in_gain(data->codec_device, db) == ESP_CODEC_DEV_OK) ? ERROR_NONE : ERROR_RESOURCE;
+}
+
+error_t get_volume(Device* device, AudioCodecDirection direction, float* volume_percent) {
+ auto* data = GET_DATA(device);
+ if (data->codec_device == nullptr || direction != AUDIO_CODEC_DIR_INPUT) {
+ return ERROR_NOT_SUPPORTED;
+ }
+
+ float db = 0.0f;
+ if (esp_codec_dev_get_in_gain(data->codec_device, &db) != ESP_CODEC_DEV_OK) {
+ return ERROR_RESOURCE;
+ }
+ *volume_percent = (db / MAX_INPUT_GAIN_DB) * 100.0f;
+ return ERROR_NONE;
+}
+
+error_t set_mute(Device* device, AudioCodecDirection direction, bool muted) {
+ auto* data = GET_DATA(device);
+ if (data->codec_device == nullptr || direction != AUDIO_CODEC_DIR_INPUT) {
+ return ERROR_NOT_SUPPORTED;
+ }
+
+ return (esp_codec_dev_set_in_mute(data->codec_device, muted) == ESP_CODEC_DEV_OK) ? ERROR_NONE : ERROR_RESOURCE;
+}
+
+error_t get_mute(Device* device, AudioCodecDirection direction, bool* muted) {
+ auto* data = GET_DATA(device);
+ if (data->codec_device == nullptr || direction != AUDIO_CODEC_DIR_INPUT) {
+ return ERROR_NOT_SUPPORTED;
+ }
+
+ return (esp_codec_dev_get_in_mute(data->codec_device, muted) == ESP_CODEC_DEV_OK) ? ERROR_NONE : ERROR_RESOURCE;
+}
+
+error_t get_native_channels(Device* device, AudioCodecDirection direction, uint8_t* channels) {
+ (void) device;
+ if (direction != AUDIO_CODEC_DIR_INPUT) {
+ return ERROR_NOT_SUPPORTED;
+ }
+ *channels = NATIVE_CHANNELS;
+ return ERROR_NONE;
+}
+
+error_t get_native_sample_rate(Device* device, AudioCodecDirection direction, uint32_t* rate_hz) {
+ (void) device;
+ if (direction != AUDIO_CODEC_DIR_INPUT) {
+ return ERROR_NOT_SUPPORTED;
+ }
+ *rate_hz = NATIVE_SAMPLE_RATE;
+ return ERROR_NONE;
+}
+
+error_t get_capabilities(Device* device, AudioCodecDirection* supported_directions) {
+ (void) device;
+ *supported_directions = AUDIO_CODEC_DIR_INPUT;
+ return ERROR_NONE;
+}
+
+error_t get_input_gain_multiplier(Device* device, float* gain) {
+ *gain = GET_DATA(device)->input_gain;
+ return ERROR_NONE;
+}
+
+static const struct AudioCodecApi API = {
+ .open = open,
+ .close = close,
+ .read = read,
+ .write = write,
+ .set_volume = set_volume,
+ .get_volume = get_volume,
+ .set_mute = set_mute,
+ .get_mute = get_mute,
+ .get_native_sample_rate = get_native_sample_rate,
+ .get_native_channels = get_native_channels,
+ .get_capabilities = get_capabilities,
+ .get_input_gain_multiplier = get_input_gain_multiplier,
+};
+
+// endregion
+
+// region Driver lifecycle
+
+void delete_interfaces(Es7243eData* data) {
+ // The delete functions close their interface first
+ if (data->codec_device != nullptr) {
+ esp_codec_dev_delete(data->codec_device);
+ }
+ if (data->codec_if != nullptr) {
+ audio_codec_delete_codec_if(data->codec_if);
+ }
+ if (data->data_if != nullptr) {
+ audio_codec_delete_data_if(data->data_if);
+ }
+ if (data->ctrl_if != nullptr) {
+ audio_codec_delete_ctrl_if(data->ctrl_if);
+ }
+}
+
+error_t start_device(Device* device) {
+ const auto* config = GET_CONFIG(device);
+
+ auto* i2c_controller = device_get_parent(device);
+ if (i2c_controller == nullptr || device_get_type(i2c_controller) != &I2C_CONTROLLER_TYPE) {
+ LOG_E(TAG, "Parent is not an I2C controller");
+ return ERROR_RESOURCE;
+ }
+
+ auto* i2s_controller = config->i2s_device;
+ if (i2s_controller == nullptr || device_get_type(i2s_controller) != &I2S_CONTROLLER_TYPE) {
+ LOG_E(TAG, "I2S controller device is not valid");
+ return ERROR_RESOURCE;
+ }
+
+ auto* data = new Es7243eData();
+ data->input_gain = (float) config->input_gain_percent / 100.0f;
+
+ data->ctrl_if = audio_codec_adapter_new_i2c_ctrl(i2c_controller, config->address);
+ data->data_if = audio_codec_adapter_new_i2s_data(i2s_controller);
+ bool ok = data->ctrl_if != nullptr && data->data_if != nullptr;
+ if (!ok) {
+ LOG_E(TAG, "Failed to create adapters");
+ }
+
+ if (ok && data->ctrl_if->open(data->ctrl_if, nullptr, 0) != ESP_CODEC_DEV_OK) {
+ LOG_E(TAG, "Failed to open control interface");
+ ok = false;
+ }
+
+ if (ok && data->data_if->open(data->data_if, nullptr, 0) != ESP_CODEC_DEV_OK) {
+ LOG_E(TAG, "Failed to open data interface");
+ ok = false;
+ }
+
+ if (ok) {
+ es7243e_codec_cfg_t codec_config = {};
+ codec_config.ctrl_if = data->ctrl_if;
+ data->codec_if = es7243e_codec_new(&codec_config);
+ if (data->codec_if == nullptr) {
+ LOG_E(TAG, "Failed to create ES7243E codec interface");
+ ok = false;
+ }
+ }
+
+ if (ok) {
+ esp_codec_dev_cfg_t dev_config = {
+ .dev_type = ESP_CODEC_DEV_TYPE_IN,
+ .codec_if = data->codec_if,
+ .data_if = data->data_if,
+ };
+ data->codec_device = esp_codec_dev_new(&dev_config);
+ if (data->codec_device == nullptr) {
+ LOG_E(TAG, "Failed to create codec device");
+ ok = false;
+ }
+ }
+
+ if (!ok) {
+ delete_interfaces(data);
+ delete data;
+ return ERROR_RESOURCE;
+ }
+
+ device_set_driver_data(device, data);
+ return ERROR_NONE;
+}
+
+error_t stop_device(Device* device) {
+ auto* data = GET_DATA(device);
+ if (data == nullptr) {
+ return ERROR_NONE;
+ }
+
+ if (data->is_open) {
+ esp_codec_dev_close(data->codec_device);
+ }
+ delete_interfaces(data);
+
+ device_set_driver_data(device, nullptr);
+ delete data;
+ return ERROR_NONE;
+}
+
+// endregion
+
+} // namespace
+
+extern "C" {
+
+Driver es7243e_driver = {
+ .name = "es7243e",
+ .compatible = (const char*[]) { "everest,es7243e", nullptr },
+ .start_device = start_device,
+ .stop_device = stop_device,
+ .probe = nullptr,
+ .api = &API,
+ .device_type = &AUDIO_CODEC_TYPE,
+ .owner = &es7243e_module,
+ .internal = nullptr,
+};
+
+}
diff --git a/Drivers/es7243e-module/source/module.cpp b/Drivers/es7243e-module/source/module.cpp
new file mode 100644
index 000000000..3c722c252
--- /dev/null
+++ b/Drivers/es7243e-module/source/module.cpp
@@ -0,0 +1,23 @@
+// SPDX-License-Identifier: Apache-2.0
+#include
+#include
+
+extern "C" {
+
+extern Driver es7243e_driver;
+
+static Driver* const es7243e_drivers[] = {
+ &es7243e_driver,
+ nullptr
+};
+
+Module es7243e_module = {
+ .name = "es7243e",
+ .start = nullptr,
+ .stop = nullptr,
+ .drivers = es7243e_drivers,
+ .symbols = nullptr,
+ .internal = nullptr
+};
+
+} // extern "C"
From b07623f9aff3e6b0df01664c63fead853f065553 Mon Sep 17 00:00:00 2001
From: AdaSzi115026 <81822538+AdaSzi@users.noreply.github.com>
Date: Mon, 5 Oct 2026 23:54:34 +0200
Subject: [PATCH 4/5] Honor reset polarity and pulse count on the GT911
expander path
A native reset pin combined with an expander interrupt pin now keeps its configured polarity and the reset is pulsed reset-pulses times like on the native path.
---
Drivers/gt911-module/source/gt911.cpp | 30 +++++++++++++++++----------
1 file changed, 19 insertions(+), 11 deletions(-)
diff --git a/Drivers/gt911-module/source/gt911.cpp b/Drivers/gt911-module/source/gt911.cpp
index d6feadc71..540e0673a 100644
--- a/Drivers/gt911-module/source/gt911.cpp
+++ b/Drivers/gt911-module/source/gt911.cpp
@@ -74,9 +74,12 @@ static bool is_expander_pin(const GpioPinSpec& pin) {
}
// esp_lcd_touch only accepts native pins, so the reset is done here. INT is held low while reset
-// is released, which selects address 0x5D. Physical levels are used because IO expanders reject
-// ACTIVE_LOW on outputs.
-static error_t reset_with_expander_pins(const GpioPinSpec& reset_pin, const GpioPinSpec& interrupt_pin) {
+// is released, which selects address 0x5D.
+static error_t reset_with_expander_pins(const GpioPinSpec& reset_pin, const GpioPinSpec& interrupt_pin, uint8_t pulses) {
+ if (pulses == 0) {
+ return ERROR_NONE;
+ }
+
GpioDescriptor* interrupt = nullptr;
if (interrupt_pin.gpio_controller != nullptr) {
interrupt = gpio_descriptor_acquire(interrupt_pin.gpio_controller, interrupt_pin.pin, GPIO_FLAG_DIRECTION_OUTPUT, GPIO_OWNER_GPIO);
@@ -86,9 +89,14 @@ static error_t reset_with_expander_pins(const GpioPinSpec& reset_pin, const Gpio
}
}
+ // A native reset pin keeps its configured polarity. An expander pin is driven active-low at
+ // physical levels because IO expanders reject ACTIVE_LOW on outputs.
+ bool reset_on_expander = is_expander_pin(reset_pin);
+ bool asserted_level = !reset_on_expander;
GpioDescriptor* reset = nullptr;
if (reset_pin.gpio_controller != nullptr) {
- reset = gpio_descriptor_acquire(reset_pin.gpio_controller, reset_pin.pin, GPIO_FLAG_DIRECTION_OUTPUT, GPIO_OWNER_GPIO);
+ gpio_flags_t reset_flags = reset_on_expander ? GPIO_FLAG_DIRECTION_OUTPUT : (reset_pin.flags | GPIO_FLAG_DIRECTION_OUTPUT);
+ reset = gpio_descriptor_acquire(reset_pin.gpio_controller, reset_pin.pin, reset_flags, GPIO_OWNER_GPIO);
if (reset == nullptr) {
LOG_E(TAG, "Failed to acquire reset pin");
if (interrupt != nullptr) {
@@ -102,15 +110,15 @@ static error_t reset_with_expander_pins(const GpioPinSpec& reset_pin, const Gpio
if (interrupt != nullptr) {
error = gpio_descriptor_set_level(interrupt, false);
}
- if (error == ERROR_NONE && reset != nullptr) {
- error = gpio_descriptor_set_level(reset, false);
+ for (uint8_t i = 0; error == ERROR_NONE && reset != nullptr && i < pulses; i++) {
+ error = gpio_descriptor_set_level(reset, asserted_level);
+ if (error == ERROR_NONE) {
+ vTaskDelay(pdMS_TO_TICKS(10));
+ error = gpio_descriptor_set_level(reset, !asserted_level);
+ }
if (error == ERROR_NONE) {
vTaskDelay(pdMS_TO_TICKS(10));
- error = gpio_descriptor_set_level(reset, true);
}
- }
- if (error == ERROR_NONE) {
- vTaskDelay(pdMS_TO_TICKS(10));
}
if (interrupt != nullptr) {
@@ -215,7 +223,7 @@ static error_t start(Device* device) {
bool expander_pins = is_expander_pin(config->pin_reset) || is_expander_pin(config->pin_interrupt);
error_t error = expander_pins
- ? reset_with_expander_pins(config->pin_reset, config->pin_interrupt)
+ ? reset_with_expander_pins(config->pin_reset, config->pin_interrupt, config->reset_pulses)
: reset_controller_pin(config->pin_reset, config->reset_pulses);
if (error != ERROR_NONE) {
free(internal);
From a3a18dd0374ec56a7cbed8b1b25f90befd40a56b Mon Sep 17 00:00:00 2001
From: AdaSzi115026 <81822538+AdaSzi@users.noreply.github.com>
Date: Tue, 6 Oct 2026 00:28:37 +0200
Subject: [PATCH 5/5] Support active-low outputs in the TCA95xx driver and
address review notes
- TCA95xx: active-low outputs are inverted in software. The GT911 and NV3031B drivers use that for their reset pins instead of driving physical levels and the device sets GPIO_FLAG_ACTIVE_LOW on the touch reset pin. ideas.md notes the same limitation in xl9555 and tca9534.
- LP5814: document the units of the current settings.
- New drivers use constexpr for TAG.
- Device: drive expander pin 4 high like the vendor firmware. Without it the audio codecs do not answer after a cold boot.
---
.../seeed,wio-tracker-l2-pro.dts | 10 +++++++++-
Documentation/ideas.md | 2 ++
Drivers/ads1115-module/source/ads1115.cpp | 2 +-
Drivers/es7243e-module/source/es7243e.cpp | 2 +-
Drivers/gt911-module/bindings/goodix,gt911.yaml | 4 ++--
Drivers/gt911-module/source/gt911.cpp | 15 +++++----------
Drivers/lp5814-module/bindings/ti,lp5814.yaml | 6 +++---
Drivers/lp5814-module/include/drivers/lp5814.h | 3 +++
Drivers/lp5814-module/source/lp5814.cpp | 2 +-
.../bindings/newvision,nv3031b.yaml | 2 +-
Drivers/nv3031b-module/include/drivers/nv3031b.h | 2 +-
Drivers/nv3031b-module/source/nv3031b.cpp | 8 ++++----
Drivers/tca95xx-16bit-module/README.md | 3 ++-
Drivers/tca95xx-16bit-module/source/tca95xx.cpp | 13 ++++---------
14 files changed, 39 insertions(+), 35 deletions(-)
diff --git a/Devices/seeed-wio-tracker-l2-pro/seeed,wio-tracker-l2-pro.dts b/Devices/seeed-wio-tracker-l2-pro/seeed,wio-tracker-l2-pro.dts
index ac7205df8..925fb1629 100644
--- a/Devices/seeed-wio-tracker-l2-pro/seeed,wio-tracker-l2-pro.dts
+++ b/Devices/seeed-wio-tracker-l2-pro/seeed,wio-tracker-l2-pro.dts
@@ -65,6 +65,13 @@
compatible = "ti,tca9535";
reg = <0x21>;
+ // Driven high by the vendor firmware. After a cold boot the audio codecs only answer with this high.
+ pin4 {
+ compatible = "gpio-hog";
+ pin = <&pca9555 4 GPIO_FLAG_NONE>;
+ mode = ;
+ };
+
lcd_power {
compatible = "gpio-hog";
pin = <&pca9555 5 GPIO_FLAG_NONE>;
@@ -129,7 +136,7 @@
y-max = <320>;
swap-xy;
mirror-y;
- pin-reset = <&pca9555 8 GPIO_FLAG_NONE>;
+ pin-reset = <&pca9555 8 GPIO_FLAG_ACTIVE_LOW>;
pin-interrupt = <&pca9555 3 GPIO_FLAG_NONE>;
};
@@ -159,6 +166,7 @@
compatible = "ti,lp5814";
reg = <0x2C>;
status = "disabled";
+ // 40 mA per output, as in the vendor firmware
high-current;
dot-current = <200>;
};
diff --git a/Documentation/ideas.md b/Documentation/ideas.md
index 28f10e2e2..da100cdf8 100644
--- a/Documentation/ideas.md
+++ b/Documentation/ideas.md
@@ -20,6 +20,8 @@
Check during installation process, but also when starting (SD card might have old app install from before Tactility OS update)
- Support direct installation of an `.app` file with `tactility.py install --app helloworld.app`
- minitar/untarFile(): "entry->metadata.path" can escape its confined path (e.g. "../something")
+- xl9555 and tca9534 drivers: support `GPIO_FLAG_ACTIVE_LOW` on outputs in software, like tca95xx does.
+ Afterwards the raw level reset handling in cst66xx and tca8418 can use ACTIVE_LOW too.
## Medium Priority
diff --git a/Drivers/ads1115-module/source/ads1115.cpp b/Drivers/ads1115-module/source/ads1115.cpp
index 9d113b36a..b07806d85 100644
--- a/Drivers/ads1115-module/source/ads1115.cpp
+++ b/Drivers/ads1115-module/source/ads1115.cpp
@@ -10,7 +10,7 @@
#include
#include
-#define TAG "ADS1115"
+constexpr auto* TAG = "ADS1115";
#define GET_CONFIG(device) (static_cast((device)->config))
static constexpr uint8_t REG_CONVERSION = 0x00;
diff --git a/Drivers/es7243e-module/source/es7243e.cpp b/Drivers/es7243e-module/source/es7243e.cpp
index 3d7ccf8ba..f4be594ae 100644
--- a/Drivers/es7243e-module/source/es7243e.cpp
+++ b/Drivers/es7243e-module/source/es7243e.cpp
@@ -14,7 +14,7 @@
#include
#include
-#define TAG "ES7243E"
+constexpr auto* TAG = "ES7243E";
namespace {
diff --git a/Drivers/gt911-module/bindings/goodix,gt911.yaml b/Drivers/gt911-module/bindings/goodix,gt911.yaml
index 8bb094cb2..5506ec6e4 100644
--- a/Drivers/gt911-module/bindings/goodix,gt911.yaml
+++ b/Drivers/gt911-module/bindings/goodix,gt911.yaml
@@ -33,8 +33,8 @@ properties:
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). A pin on an IO expander is driven
- active-low at physical levels, with the interrupt pin held low during reset (address 0x5D).
+ reset line, e.g. Tulip 4 R11, use ACTIVE_HIGH). Pins on an IO expander are supported, the
+ driver then pulses the reset itself with the interrupt pin held low (address 0x5D).
pin-interrupt:
type: phandles
default: GPIO_PIN_SPEC_NONE
diff --git a/Drivers/gt911-module/source/gt911.cpp b/Drivers/gt911-module/source/gt911.cpp
index 540e0673a..3e74f78c3 100644
--- a/Drivers/gt911-module/source/gt911.cpp
+++ b/Drivers/gt911-module/source/gt911.cpp
@@ -73,8 +73,8 @@ static bool is_expander_pin(const GpioPinSpec& pin) {
return pin.gpio_controller != nullptr && !driver_is_compatible(device_get_driver(pin.gpio_controller), "espressif,esp32-gpio");
}
-// esp_lcd_touch only accepts native pins, so the reset is done here. INT is held low while reset
-// is released, which selects address 0x5D.
+// esp_lcd_touch only accepts native pins, so the reset is done here with the reset polarity from
+// the pin flags. INT is held physically low while reset is released, which selects address 0x5D.
static error_t reset_with_expander_pins(const GpioPinSpec& reset_pin, const GpioPinSpec& interrupt_pin, uint8_t pulses) {
if (pulses == 0) {
return ERROR_NONE;
@@ -89,14 +89,9 @@ static error_t reset_with_expander_pins(const GpioPinSpec& reset_pin, const Gpio
}
}
- // A native reset pin keeps its configured polarity. An expander pin is driven active-low at
- // physical levels because IO expanders reject ACTIVE_LOW on outputs.
- bool reset_on_expander = is_expander_pin(reset_pin);
- bool asserted_level = !reset_on_expander;
GpioDescriptor* reset = nullptr;
if (reset_pin.gpio_controller != nullptr) {
- gpio_flags_t reset_flags = reset_on_expander ? GPIO_FLAG_DIRECTION_OUTPUT : (reset_pin.flags | GPIO_FLAG_DIRECTION_OUTPUT);
- reset = gpio_descriptor_acquire(reset_pin.gpio_controller, reset_pin.pin, reset_flags, GPIO_OWNER_GPIO);
+ reset = gpio_descriptor_acquire(reset_pin.gpio_controller, reset_pin.pin, reset_pin.flags | GPIO_FLAG_DIRECTION_OUTPUT, GPIO_OWNER_GPIO);
if (reset == nullptr) {
LOG_E(TAG, "Failed to acquire reset pin");
if (interrupt != nullptr) {
@@ -111,10 +106,10 @@ static error_t reset_with_expander_pins(const GpioPinSpec& reset_pin, const Gpio
error = gpio_descriptor_set_level(interrupt, false);
}
for (uint8_t i = 0; error == ERROR_NONE && reset != nullptr && i < pulses; i++) {
- error = gpio_descriptor_set_level(reset, asserted_level);
+ error = gpio_descriptor_set_level(reset, true);
if (error == ERROR_NONE) {
vTaskDelay(pdMS_TO_TICKS(10));
- error = gpio_descriptor_set_level(reset, !asserted_level);
+ error = gpio_descriptor_set_level(reset, false);
}
if (error == ERROR_NONE) {
vTaskDelay(pdMS_TO_TICKS(10));
diff --git a/Drivers/lp5814-module/bindings/ti,lp5814.yaml b/Drivers/lp5814-module/bindings/ti,lp5814.yaml
index 6ab72a48c..6d02375e0 100644
--- a/Drivers/lp5814-module/bindings/ti,lp5814.yaml
+++ b/Drivers/lp5814-module/bindings/ti,lp5814.yaml
@@ -16,12 +16,12 @@ properties:
high-current:
type: boolean
default: false
- description: Use the 51 mA maximum output current range instead of 25.5 mA
+ description: Use a maximum output current of 51 mA instead of 25.5 mA
dot-current:
type: int
default: 255
- description: Output current as a fraction of the maximum current [0,255]
+ description: Output current per enabled output in 1/255 of the maximum current [0,255], so 0.2 mA steps at 51 mA and 0.1 mA steps at 25.5 mA
brightness-default:
type: int
default: 255
- description: Default brightness level, applied by set_brightness_default()
+ description: Default brightness level (PWM duty in 1/255), applied by set_brightness_default()
diff --git a/Drivers/lp5814-module/include/drivers/lp5814.h b/Drivers/lp5814-module/include/drivers/lp5814.h
index 076f0eea7..99dc29862 100644
--- a/Drivers/lp5814-module/include/drivers/lp5814.h
+++ b/Drivers/lp5814-module/include/drivers/lp5814.h
@@ -12,8 +12,11 @@ struct Lp5814Config {
uint8_t address;
// Bit mask of OUT0 to OUT3
uint8_t channels;
+ // Maximum output current: 51 mA when true, 25.5 mA when false
bool high_current;
+ // Output current per enabled output in 1/255 of the maximum current: 0.2 mA steps at 51 mA, 0.1 mA steps at 25.5 mA
uint8_t dot_current;
+ // Default PWM duty in 1/255
uint8_t brightness_default;
};
diff --git a/Drivers/lp5814-module/source/lp5814.cpp b/Drivers/lp5814-module/source/lp5814.cpp
index fb0477976..1e13199be 100644
--- a/Drivers/lp5814-module/source/lp5814.cpp
+++ b/Drivers/lp5814-module/source/lp5814.cpp
@@ -12,7 +12,7 @@
#include
-#define TAG "LP5814"
+constexpr auto* TAG = "LP5814";
#define GET_CONFIG(device) (static_cast((device)->config))
#define GET_INTERNAL(device) (static_cast(device_get_driver_data(device)))
diff --git a/Drivers/nv3031b-module/bindings/newvision,nv3031b.yaml b/Drivers/nv3031b-module/bindings/newvision,nv3031b.yaml
index 7cc6854f3..f4ad1d081 100644
--- a/Drivers/nv3031b-module/bindings/newvision,nv3031b.yaml
+++ b/Drivers/nv3031b-module/bindings/newvision,nv3031b.yaml
@@ -52,7 +52,7 @@ properties:
pin-reset:
type: phandles
default: GPIO_PIN_SPEC_NONE
- description: Active-low reset pin. Pins on an IO expander are supported.
+ description: Reset pin, driven active-low by the driver. Pins on an IO expander are supported.
backlight:
type: phandle
default: "NULL"
diff --git a/Drivers/nv3031b-module/include/drivers/nv3031b.h b/Drivers/nv3031b-module/include/drivers/nv3031b.h
index 4f5b81e33..af50031af 100644
--- a/Drivers/nv3031b-module/include/drivers/nv3031b.h
+++ b/Drivers/nv3031b-module/include/drivers/nv3031b.h
@@ -23,7 +23,7 @@ struct Nv3031bConfig {
bool bgr_order;
uint32_t pixel_clock_hz;
uint8_t transaction_queue_depth;
- // Active-low reset line, may be on an IO expander.
+ // Reset line, driven active-low. May be on an IO expander.
struct GpioPinSpec pin_reset;
// Optional reference to this display's backlight device, NULL if none.
struct Device* backlight;
diff --git a/Drivers/nv3031b-module/source/nv3031b.cpp b/Drivers/nv3031b-module/source/nv3031b.cpp
index 7053364d3..2aeb207af 100644
--- a/Drivers/nv3031b-module/source/nv3031b.cpp
+++ b/Drivers/nv3031b-module/source/nv3031b.cpp
@@ -45,22 +45,22 @@ static int pin_or_unused(const struct GpioPinSpec& pin) {
return pin.gpio_controller == nullptr ? -1 : static_cast(pin.pin);
}
-// Drives physical levels because IO expanders reject ACTIVE_LOW on outputs
+// The reset line of the NV3031B is active-low
static error_t hardware_reset(const struct GpioPinSpec& pin) {
if (pin.gpio_controller == nullptr) {
return ERROR_NONE;
}
- auto* reset = gpio_descriptor_acquire(pin.gpio_controller, pin.pin, GPIO_FLAG_DIRECTION_OUTPUT, GPIO_OWNER_GPIO);
+ auto* reset = gpio_descriptor_acquire(pin.gpio_controller, pin.pin, GPIO_FLAG_DIRECTION_OUTPUT | GPIO_FLAG_ACTIVE_LOW, GPIO_OWNER_GPIO);
if (reset == nullptr) {
LOG_E(TAG, "Failed to acquire reset pin");
return ERROR_RESOURCE;
}
- error_t error = gpio_descriptor_set_level(reset, false);
+ error_t error = gpio_descriptor_set_level(reset, true);
if (error == ERROR_NONE) {
delay_millis(10);
- error = gpio_descriptor_set_level(reset, true);
+ error = gpio_descriptor_set_level(reset, false);
}
gpio_descriptor_release(reset);
if (error != ERROR_NONE) {
diff --git a/Drivers/tca95xx-16bit-module/README.md b/Drivers/tca95xx-16bit-module/README.md
index 104d13a54..7a1a5d3c6 100644
--- a/Drivers/tca95xx-16bit-module/README.md
+++ b/Drivers/tca95xx-16bit-module/README.md
@@ -6,6 +6,7 @@ pin): two 8-bit ports for input, output, polarity inversion, and direction, addr
pins 0-15 (port 0 = pins 0-7, port 1 = pins 8-15).
It does not support pull-up/down resistors or high-impedance outputs; requesting those
-flags returns `ERROR_NOT_SUPPORTED`.
+flags returns `ERROR_NOT_SUPPORTED`. Active-low pins use the polarity register for reading
+and are inverted in software when driven as outputs.
License: [Apache v2.0](LICENSE-Apache-2.0.md)
diff --git a/Drivers/tca95xx-16bit-module/source/tca95xx.cpp b/Drivers/tca95xx-16bit-module/source/tca95xx.cpp
index 7e25cabda..59e618f91 100644
--- a/Drivers/tca95xx-16bit-module/source/tca95xx.cpp
+++ b/Drivers/tca95xx-16bit-module/source/tca95xx.cpp
@@ -46,12 +46,14 @@ static error_t set_level(GpioDescriptor* descriptor, bool high) {
auto address = GET_CONFIG(device)->address;
auto reg = static_cast(TCA95XX_REGISTER_OUTPUT_PORT0 + port_of(descriptor));
auto bit = bit_of(descriptor);
+ // The output port has no polarity inversion, so active-low outputs are inverted here
+ bool physical_high = (descriptor->flags & GPIO_FLAG_ACTIVE_LOW) != 0 ? !high : high;
// i2c_controller_register8_{set,reset}_bits() do a separate read then
// write; without this lock, concurrent updates to different pins on the
// same output port register can clobber each other.
device_lock(device);
- error_t err = high
+ error_t err = physical_high
? i2c_controller_register8_set_bits(parent, address, reg, bit, portMAX_DELAY)
: i2c_controller_register8_reset_bits(parent, address, reg, bit, portMAX_DELAY);
device_unlock(device);
@@ -81,13 +83,6 @@ static error_t set_flags(GpioDescriptor* descriptor, gpio_flags_t flags) {
return ERROR_NOT_SUPPORTED;
}
- // The polarity register only inverts what's read back from an input pin;
- // set_level() still drives outputs at the raw level. Accepting ACTIVE_LOW
- // on an output would silently not do what it implies.
- if ((flags & GPIO_FLAG_ACTIVE_LOW) && (flags & GPIO_FLAG_DIRECTION_OUTPUT)) {
- return ERROR_NOT_SUPPORTED;
- }
-
auto* device = descriptor->controller;
auto* parent = device_get_parent(device);
auto address = GET_CONFIG(device)->address;
@@ -113,7 +108,7 @@ static error_t set_flags(GpioDescriptor* descriptor, gpio_flags_t flags) {
return err;
}
- // Polarity inversion (mainly relevant for active-low inputs).
+ // Input polarity inversion, so get_level() reports the logical level of active-low pins
if (flags & GPIO_FLAG_ACTIVE_LOW) {
err = i2c_controller_register8_set_bits(parent, address, polarity_reg, bit, portMAX_DELAY);
} else {