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 {