diff --git a/Devices/waveshare-s3-touch-amoled-18/CMakeLists.txt b/Devices/waveshare-s3-touch-amoled-18/CMakeLists.txt new file mode 100644 index 000000000..3321e6ff6 --- /dev/null +++ b/Devices/waveshare-s3-touch-amoled-18/CMakeLists.txt @@ -0,0 +1,19 @@ +file(GLOB_RECURSE SOURCE_FILES source/*.c*) + +idf_component_register( + SRCS ${SOURCE_FILES} + INCLUDE_DIRS "source" + REQUIRES TactilityKernel Tactility TactilityFreeRtos lvgl-module lvgl-window-manager-module esp_driver_i2c esp_driver_gpio +) + +# Board-local link-time hooks (see source/lvgl_scaling.cpp and source/lvgl_insets.cpp): keep the +# Material theme's widget sizes usable while tightening its spacing, and keep the UI clear of the +# top/bottom edges. No core Tactility/LVGL source is modified. +idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=window_manager_configure" APPEND) +idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=lv_display_create" APPEND) +idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=lv_style_set_pad_top" APPEND) +idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=lv_style_set_pad_bottom" APPEND) +idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=lv_style_set_pad_left" APPEND) +idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=lv_style_set_pad_right" APPEND) +idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=lv_style_set_pad_row" APPEND) +idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=lv_style_set_pad_column" APPEND) diff --git a/Devices/waveshare-s3-touch-amoled-18/device.properties b/Devices/waveshare-s3-touch-amoled-18/device.properties new file mode 100644 index 000000000..db3153c7d --- /dev/null +++ b/Devices/waveshare-s3-touch-amoled-18/device.properties @@ -0,0 +1,30 @@ +general.vendor=Waveshare +general.name=S3 Touch AMOLED 1.8" +general.incubating=true + +apps.launcherAppId=tactility.launcher + +hardware.target=ESP32S3 +hardware.flashSize=16MB +hardware.spiRam=true +hardware.spiRamMode=OCT +hardware.spiRamSpeed=120M +hardware.tinyUsbMsc=true +hardware.esptoolFlashFreq=120M +hardware.bluetooth=true + +storage.userDataLocation=Internal + +display.size=1.8" +display.shape=rectangle +display.dpi=322 + +lvgl.colorDepth=16 +lvgl.uiDensity=default +lvgl.fontSize=22 + +sdkconfig.CONFIG_TASK_WDT_TIMEOUT_S=10 +sdkconfig.CONFIG_SPIRAM_MALLOC_RESERVE_INTERNAL=131072 +# LVGL v9 symbol (v8 was LV_DISP_DEF_REFR_PERIOD). Upstream moved this out of device.py, +# so pin it per-device: 10 ms keeps the panel above the 16 ms animation tick. +sdkconfig.CONFIG_LV_DEF_REFR_PERIOD=10 diff --git a/Devices/waveshare-s3-touch-amoled-18/module.yaml b/Devices/waveshare-s3-touch-amoled-18/module.yaml new file mode 100644 index 000000000..3e39b4c7c --- /dev/null +++ b/Devices/waveshare-s3-touch-amoled-18/module.yaml @@ -0,0 +1,11 @@ +dependencies: + - Platforms/platform-esp32 + - Drivers/sh8601-module + - Drivers/axp2101-module + - Drivers/pcf85063-module + - Drivers/qmi8658-module + - Drivers/ft5x06-module + - Drivers/es8311-module + - Drivers/audio-stream-module + - Drivers/tca9534-module +dts: waveshare,s3-touch-amoled-18.dts diff --git a/Devices/waveshare-s3-touch-amoled-18/source/expander_reset.cpp b/Devices/waveshare-s3-touch-amoled-18/source/expander_reset.cpp new file mode 100644 index 000000000..c73137e92 --- /dev/null +++ b/Devices/waveshare-s3-touch-amoled-18/source/expander_reset.cpp @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: Apache-2.0 + +// The Waveshare 1.8" board does not wire the AMOLED panel or touch reset lines to ESP32 GPIOs. +// They hang off a TCA9554 I2C expander (address 0x20), whose pins the board's own examples drive +// low then high (EXIO0, EXIO1, EXIO2 and EXIO6) before initializing the panel and touch +// controller. Without that pulse the SH8601 panel stays blank and the FT3168 touch NAKs every +// I2C transaction (the later "FT5x06 init failed"). +// +// This runs from the board module's start(), which is before the kernel constructs and starts the +// devicetree's devices, so the reset is released ahead of the display/touch drivers' own init. It +// uses a temporary ESP-IDF I2C master (on a spare port, so it can't collide with the kernel's +// i2c0) and tears it down again, leaving the bus for i2c0 to set up afterwards. + +#include + +#include +#include +#include +#include + +namespace { + +constexpr auto* TAG = "ws-amoled18-expander"; + +constexpr uint8_t EXPANDER_ADDRESS = 0x20; + +constexpr gpio_num_t I2C_SDA = GPIO_NUM_15; +constexpr gpio_num_t I2C_SCL = GPIO_NUM_14; + +// TCA9554 register map (shared with the TCA9534). +constexpr uint8_t REG_OUTPUT = 0x01; +constexpr uint8_t REG_CONFIG = 0x03; + +// EXIO0/1/2/6 drive the panel and touch reset lines. +constexpr uint8_t RESET_PINS = (uint8_t)((1u << 0) | (1u << 1) | (1u << 2) | (1u << 6)); + +bool write_register(i2c_master_dev_handle_t device, uint8_t reg, uint8_t value) { + uint8_t buffer[2] = { reg, value }; + return i2c_master_transmit(device, buffer, sizeof(buffer), -1) == ESP_OK; +} + +} // namespace + +void waveshare_amoled18_expander_reset() { + i2c_master_bus_config_t bus_config = {}; + bus_config.clk_source = I2C_CLK_SRC_DEFAULT; + bus_config.i2c_port = I2C_NUM_1; // spare port; i2c0 belongs to the kernel devicetree + bus_config.sda_io_num = I2C_SDA; + bus_config.scl_io_num = I2C_SCL; + bus_config.glitch_ignore_cnt = 7; + bus_config.flags.enable_internal_pullup = true; + + i2c_master_bus_handle_t bus = nullptr; + esp_err_t err = i2c_new_master_bus(&bus_config, &bus); + if (err != ESP_OK) { + LOG_W(TAG, "Failed to open I2C bus: %s", esp_err_to_name(err)); + return; + } + + i2c_device_config_t device_config = {}; + device_config.dev_addr_length = I2C_ADDR_BIT_LEN_7; + device_config.device_address = EXPANDER_ADDRESS; + device_config.scl_speed_hz = 400000; + + i2c_master_dev_handle_t device = nullptr; + err = i2c_master_bus_add_device(bus, &device_config, &device); + if (err == ESP_OK) { + // EXIO0/1/2/6 as outputs (config bit 0 = output), the rest left as inputs; then the + // reset pulse itself. + if (!write_register(device, REG_CONFIG, (uint8_t)~RESET_PINS) || + !write_register(device, REG_OUTPUT, 0x00)) { + LOG_W(TAG, "Expander reset: initial writes failed"); + } + vTaskDelay(pdMS_TO_TICKS(20)); + if (!write_register(device, REG_OUTPUT, RESET_PINS)) { + LOG_W(TAG, "Expander reset: release write failed"); + } else { + LOG_I(TAG, "Panel/touch reset released via TCA9554"); + } + i2c_master_bus_rm_device(device); + } else { + LOG_W(TAG, "Failed to add expander device: %s", esp_err_to_name(err)); + } + + i2c_del_master_bus(bus); +} diff --git a/Devices/waveshare-s3-touch-amoled-18/source/lvgl_insets.cpp b/Devices/waveshare-s3-touch-amoled-18/source/lvgl_insets.cpp new file mode 100644 index 000000000..60a39495a --- /dev/null +++ b/Devices/waveshare-s3-touch-amoled-18/source/lvgl_insets.cpp @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: Apache-2.0 + +// Keeps this board's UI clear of the display edges with a fixed inset. +// +// Core Tactility only applies the insets it derives from the display shape for round displays (see +// Tactility/Source/lvgl/Lvgl.cpp), and this rectangular AMOLED reports no shape, so instead of +// changing core source this wraps the window-manager screen-init callback - the same board-local +// link-time wrapping used in lvgl_scaling.cpp. +// +// The padding is applied to the window-manager root widget (the argument the screen-init callback +// receives and builds its chrome under), not just to the app container it returns. That way the +// statusbar, which the callback adds as a sibling of the app container, is inset with the rest of +// the UI instead of sitting on the top edge of the display. +// +// lv_obj_set_style_pad_all() writes the local style prop directly, so it is NOT affected by the +// /8 padding wrapper in lvgl_scaling.cpp (that one only covers lv_style_set_pad_*). + +#include + +#include + +#include + +namespace { + +// Fixed inset, in pixels, applied on all four edges. +constexpr int32_t INSET_PX = 22; + +// The callback passed to window_manager_configure(), invoked later to build the window tree. +WindowManagerScreenInitFn original_screen_init = nullptr; + +} // namespace + +extern "C" { + +static lv_obj_t* wrapping_screen_init(lv_obj_t* root_widget) { + lv_obj_set_style_pad_all(root_widget, INSET_PX, LV_STATE_DEFAULT); + return original_screen_init != nullptr ? original_screen_init(root_widget) : root_widget; +} + +void __real_window_manager_configure(WindowManagerScreenInitFn screen_init); + +void __wrap_window_manager_configure(WindowManagerScreenInitFn screen_init) { + original_screen_init = screen_init; + __real_window_manager_configure(screen_init != nullptr ? wrapping_screen_init : nullptr); +} + +} // extern "C" diff --git a/Devices/waveshare-s3-touch-amoled-18/source/lvgl_scaling.cpp b/Devices/waveshare-s3-touch-amoled-18/source/lvgl_scaling.cpp new file mode 100644 index 000000000..204b3ac2f --- /dev/null +++ b/Devices/waveshare-s3-touch-amoled-18/source/lvgl_scaling.cpp @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: Apache-2.0 + +// Tactility's Material themes (Modules/lvgl-module) derive both widget metrics (switch/checkbox/ +// slider sizes, radius, borders, ...) and spacing (padding/gaps) from the display DPI. On this +// high-DPI panel (`display.dpi` = 322) those metrics are far too large relative to the text, but +// simply lowering the DPI to tighten the spacing also shrinks the widgets to unusable sizes - the +// switch track, for example, is LV_DPX_CALC(dpi, 52) x LV_DPX_CALC(dpi, 32). +// +// So the two are decoupled here, board-locally, with link-time wrappers (the same mechanism used +// elsewhere) instead of editing core Tactility/LVGL source: +// - lv_display_create(): the display DPI tracks the text font, so widget sizes stay usable; +// - lv_style_set_pad_*(): the theme's style paddings/gaps are divided, so spacing stays tight. +// Only the two theme files use style-based padding (apps use lv_obj_set_style_*, which does not go +// through these setters), so the padding wrappers affect the theme and nothing else. + +#include + +#include +#include + +constexpr auto* TAG = "waveshare-s3-touch-amoled-18"; + +namespace { + +// Widget metrics scale with the display DPI; this puts them near Material's baseline at the +// default 22 px font (e.g. the switch track is ~50 x 31 px). +constexpr int32_t WIDGET_DPI_PER_FONT_PIXEL = 7; + +// Spacing is divided by this instead of following the widget DPI (e.g. the theme's largest control +// padding, dp(24), becomes ~3 px). +constexpr int32_t PADDING_DIVISOR = 8; + +int32_t scalePadding(int32_t value) { + // Keep zero and negative values (handle/knob geometry) as the theme designed them. + if (value <= 0) { + return value; + } + return (value + PADDING_DIVISOR / 2) / PADDING_DIVISOR; +} + +int32_t displayDpiForFontSize(int32_t font_size) { + return font_size * WIDGET_DPI_PER_FONT_PIXEL; +} + +} // namespace + +extern "C" { + +lv_display_t* __real_lv_display_create(int32_t hor_res, int32_t ver_res); + +lv_display_t* __wrap_lv_display_create(int32_t hor_res, int32_t ver_res) { + lv_display_t* display = __real_lv_display_create(hor_res, ver_res); + if (display == nullptr) { + return display; + } + + const int32_t font_size = static_cast(lvgl_get_text_font_height(FONT_SIZE_DEFAULT)); + if (font_size <= 0) { + return display; + } + + const int32_t dpi = displayDpiForFontSize(font_size); + if (lv_display_get_dpi(display) != dpi) { + lv_display_set_dpi(display, dpi); + LOG_I(TAG, "LVGL display DPI set to %d for text font %d px", (int)dpi, (int)font_size); + } + + return display; +} + +// lv_style_set_pad_all/hor/ver/gap are static inline and call these base setters, so wrapping the +// six base ones covers every padding a theme style sets. +#define WRAP_PADDING_SETTER(name) \ + void __real_##name(lv_style_t* style, int32_t value); \ + void __wrap_##name(lv_style_t* style, int32_t value) { __real_##name(style, scalePadding(value)); } + +WRAP_PADDING_SETTER(lv_style_set_pad_top) +WRAP_PADDING_SETTER(lv_style_set_pad_bottom) +WRAP_PADDING_SETTER(lv_style_set_pad_left) +WRAP_PADDING_SETTER(lv_style_set_pad_right) +WRAP_PADDING_SETTER(lv_style_set_pad_row) +WRAP_PADDING_SETTER(lv_style_set_pad_column) + +#undef WRAP_PADDING_SETTER + +} // extern "C" diff --git a/Devices/waveshare-s3-touch-amoled-18/source/module.cpp b/Devices/waveshare-s3-touch-amoled-18/source/module.cpp new file mode 100644 index 000000000..4a25a2359 --- /dev/null +++ b/Devices/waveshare-s3-touch-amoled-18/source/module.cpp @@ -0,0 +1,28 @@ +#include + +// Defined in expander_reset.cpp (C++ linkage). +void waveshare_amoled18_expander_reset(); + +extern "C" { + +static error_t start() { + // The panel/touch reset lines hang off the TCA9554 expander, so release them before the kernel + // starts the devicetree's display and touch devices. + waveshare_amoled18_expander_reset(); + return ERROR_NONE; +} + +static error_t stop() { + return ERROR_NONE; +} + +Module waveshare_s3_touch_amoled_18_module = { + .name = "waveshare-s3-touch-amoled-18", + .start = start, + .stop = stop, + .drivers = nullptr, + .symbols = nullptr, + .internal = nullptr +}; + +} diff --git a/Devices/waveshare-s3-touch-amoled-18/waveshare,s3-touch-amoled-18.dts b/Devices/waveshare-s3-touch-amoled-18/waveshare,s3-touch-amoled-18.dts new file mode 100644 index 000000000..d65906f8c --- /dev/null +++ b/Devices/waveshare-s3-touch-amoled-18/waveshare,s3-touch-amoled-18.dts @@ -0,0 +1,155 @@ +/dts-v1/; + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Reference: https://www.waveshare.com/wiki/ESP32-S3-Touch-AMOLED-1.8 +/ { + compatible = "root"; + model = "Waveshare S3 Touch AMOLED 1.8"; + + wifi0 { + compatible = "espressif,esp32-wifi-pinned"; + }; + + ble0 { + compatible = "espressif,esp32-ble"; + }; + + gpio0 { + compatible = "espressif,esp32-gpio"; + gpio-count = <49>; + }; + + // ES8311 speaker amplifier enable, active-high on GPIO46. + amp_enable { + compatible = "gpio-hog"; + pin = <&gpio0 46 GPIO_FLAG_NONE>; + mode = ; + }; + + // i2s0 must be before i2c0 so the codec can reference it. + i2s0 { + compatible = "espressif,esp32-i2s"; + port = ; + pin-bclk = <&gpio0 9 GPIO_FLAG_NONE>; + pin-ws = <&gpio0 45 GPIO_FLAG_NONE>; + pin-data-out = <&gpio0 8 GPIO_FLAG_NONE>; + pin-data-in = <&gpio0 10 GPIO_FLAG_NONE>; + pin-mclk = <&gpio0 16 GPIO_FLAG_NONE>; + }; + + i2c0 { + compatible = "espressif,esp32-i2c-master"; + port = ; + clock-frequency = <100000>; + pin-sda = <&gpio0 15 GPIO_FLAG_PULL_UP>; + pin-scl = <&gpio0 14 GPIO_FLAG_PULL_UP>; + + qmi8658 { + compatible = "qst,qmi8658"; + reg = <0x6B>; + }; + + // AXP2101 PMIC. The board runs on the chip's own power-up rail defaults: Waveshare's + // BSP and examples never reconfigure the LDOs, so no rail overrides are set here. + axp2101: axp2101 { + compatible = "x-powers,axp2101"; + reg = <0x34>; + }; + + // PCF85063A RTC at 0x51. This board's RTC is not the 2.06's BM8563: the PCF85063 + // keeps its time registers at 0x04-0x0A (not 0x02-0x08), so it needs the pcf85063 driver. + pcf85063: pcf85063 { + compatible = "nxp,pcf85063a"; + reg = <0x51>; + }; + + // FT3168, register-compatible with the FT5x06. There is no dedicated reset pin. + touch0 { + compatible = "focaltech,ft5x06"; + reg = <0x38>; + x-max = <368>; + y-max = <448>; + pin-interrupt = <&gpio0 21 GPIO_FLAG_NONE>; + disable-monitor-timeout; + }; + + es8311: es8311 { + compatible = "everest,es8311"; + reg = <0x18>; + i2s = <&i2s0>; + }; + + // TCA9554 8-bit I/O expander, register-compatible with the TCA9534 driver. + tca9554: tca9554 { + compatible = "ti,tca9534"; + reg = <0x20>; + }; + }; + + // AMOLED display on QSPI: CS=12, PCLK=11, D0..D3=4..7. No hardware reset pin. + spi2 { + compatible = "espressif,esp32-spi"; + host = ; + cs-gpios = <&gpio0 12 GPIO_FLAG_NONE>; + pin-mosi = <&gpio0 4 GPIO_FLAG_NONE>; + pin-miso = <&gpio0 5 GPIO_FLAG_NONE>; + pin-wp = <&gpio0 6 GPIO_FLAG_NONE>; + pin-hd = <&gpio0 7 GPIO_FLAG_NONE>; + pin-sclk = <&gpio0 11 GPIO_FLAG_NONE>; + max-transfer-size = <8192>; + + display0: display@0 { + compatible = "shenghe,sh8601"; + horizontal-resolution = <368>; + vertical-resolution = <448>; + gap-x = <0>; + gap-y = <0>; + pixel-clock-hz = <40000000>; + + // The SH8601 has no backlight pin; brightness is a panel command. The driver finds + // this child device via the display's children, and the backlight uses its parent. + display_backlight { + compatible = "sh8601-backlight"; + brightness-default = <255>; + }; + }; + }; + + // TF card on SDMMC (1-bit): CLK=2, CMD=1, D0=3. + sdmmc0 { + compatible = "espressif,esp32-sdmmc"; + pin-clk = <&gpio0 2 GPIO_FLAG_NONE>; + pin-cmd = <&gpio0 1 GPIO_FLAG_NONE>; + pin-d0 = <&gpio0 3 GPIO_FLAG_NONE>; + slot = ; + bus-width = <1>; + max-freq-khz = <20000>; + pullups; + }; + + usbdevice0 { + compatible = "espressif,esp32-usbdevice"; + + usbdevicemsc0 { + compatible = "espressif,esp32-usbdevice-msc"; + }; + }; +}; diff --git a/Drivers/ft5x06-module/source/ft5x06.cpp b/Drivers/ft5x06-module/source/ft5x06.cpp index 3bbde1f85..f9a44b971 100644 --- a/Drivers/ft5x06-module/source/ft5x06.cpp +++ b/Drivers/ft5x06-module/source/ft5x06.cpp @@ -24,6 +24,16 @@ #define TAG "FT5x06" #define GET_CONFIG(device) (static_cast((device)->config)) +// FT5x06/FT3168 registers (same values esp_lcd_touch_ft5x06.c uses). +static constexpr uint8_t REG_MONITOR_TIMEOUT = 0x87; // seconds before monitor mode; 0 = never +static constexpr uint8_t REG_POWER_MODE = 0xA5; // 0 = active, 1 = monitor, 2 = standby, 3 = hibernate +static constexpr uint8_t REG_STATE = 0xA7; +static constexpr uint8_t REG_ERROR = 0xA9; + +// The esp_lcd I2C touch IO defaults to 0 (= wait forever), so a stuck bus would block the LVGL +// task indefinitely. Bound it; 100 ms is far more than any single touch transaction needs. +static constexpr int TOUCH_IO_TIMEOUT_MS = 100; + struct Ft5x06Internal { esp_lcd_panel_io_handle_t io_handle; esp_lcd_touch_handle_t touch_handle; @@ -67,6 +77,7 @@ static error_t pulse_reset(GpioDescriptor* descriptor) { // so no bus probing is needed here. static esp_err_t create_io_handle(Device* parent, esp_lcd_panel_io_handle_t* out_handle) { esp_lcd_panel_io_i2c_config_t io_config = ESP_LCD_TOUCH_IO_I2C_FT5x06_CONFIG(); + io_config.transaction_timeout_ms = TOUCH_IO_TIMEOUT_MS; auto* parent_driver = device_get_driver(parent); if (driver_is_compatible(parent_driver, "espressif,esp32-i2c-master")) { @@ -159,7 +170,7 @@ static error_t start(Device* device) { if (config->disable_monitor_timeout) { // FT5x06: 0 = never auto-enter monitor mode. uint8_t disable_monitor = 0; - if (esp_lcd_panel_io_tx_param(internal->io_handle, 0x87, &disable_monitor, 1) != ESP_OK) { + if (esp_lcd_panel_io_tx_param(internal->io_handle, REG_MONITOR_TIMEOUT, &disable_monitor, 1) != ESP_OK) { LOG_W(TAG, "Failed to disable monitor timeout"); } @@ -168,7 +179,7 @@ static error_t start(Device* device) { // into - monitor mode, where it stops answering I2C until touched, which makes the periodic // poll fail. Force active so every read is answered. uint8_t power_mode_active = 0; - if (esp_lcd_panel_io_tx_param(internal->io_handle, 0xA5, &power_mode_active, 1) != ESP_OK) { + if (esp_lcd_panel_io_tx_param(internal->io_handle, REG_POWER_MODE, &power_mode_active, 1) != ESP_OK) { LOG_W(TAG, "Failed to set active power mode"); } } diff --git a/Drivers/pcf85063-module/CMakeLists.txt b/Drivers/pcf85063-module/CMakeLists.txt new file mode 100644 index 000000000..2c82a68bd --- /dev/null +++ b/Drivers/pcf85063-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(pcf85063-module + SRCS ${SOURCE_FILES} + INCLUDE_DIRS include/ + REQUIRES TactilityKernel +) diff --git a/Drivers/pcf85063-module/LICENSE-Apache-2.0.md b/Drivers/pcf85063-module/LICENSE-Apache-2.0.md new file mode 100644 index 000000000..f5f4b8b5e --- /dev/null +++ b/Drivers/pcf85063-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/pcf85063-module/README.md b/Drivers/pcf85063-module/README.md new file mode 100644 index 000000000..6cccf50e7 --- /dev/null +++ b/Drivers/pcf85063-module/README.md @@ -0,0 +1,12 @@ +# PCF85063A I2C Driver + +A driver for the `PCF85063A` Real-Time Clock/Calendar from NXP. + +Unlike the `BM8563`/`PCF8563` (which is what this repo's `bm8563-module` targets), the +PCF85063 keeps its time registers at `0x04`–`0x0A` (seconds, minutes, hours, days, +weekdays, months, years), with the `OS` clock-integrity flag in bit 7 of the seconds +register and the century flag in bit 7 of the months register. + +See https://www.nxp.com/docs/en/data-sheet/PCF85063A.pdf + +License: [Apache v2.0](LICENSE-Apache-2.0.md) diff --git a/Drivers/pcf85063-module/bindings/nxp,pcf85063a.yaml b/Drivers/pcf85063-module/bindings/nxp,pcf85063a.yaml new file mode 100644 index 000000000..2324ca60c --- /dev/null +++ b/Drivers/pcf85063-module/bindings/nxp,pcf85063a.yaml @@ -0,0 +1,5 @@ +description: PCF85063A RTC + +include: [ "i2c-device.yaml" ] + +compatible: "nxp,pcf85063a" diff --git a/Drivers/pcf85063-module/include/bindings/pcf85063.h b/Drivers/pcf85063-module/include/bindings/pcf85063.h new file mode 100644 index 000000000..50d50626f --- /dev/null +++ b/Drivers/pcf85063-module/include/bindings/pcf85063.h @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +DEFINE_DEVICETREE(pcf85063a, struct Pcf85063Config) + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/pcf85063-module/include/drivers/pcf85063.h b/Drivers/pcf85063-module/include/drivers/pcf85063.h new file mode 100644 index 000000000..13db8b88e --- /dev/null +++ b/Drivers/pcf85063-module/include/drivers/pcf85063.h @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include +#include + +struct Device; + +#ifdef __cplusplus +extern "C" { +#endif + +struct Pcf85063Config { + /** Address on bus */ + uint8_t address; +}; + +/** + * Read the current date and time from the RTC. + * @param[in] device pcf85063 device + * @param[out] dt Pointer to RtcDateTime to populate + * @return ERROR_NONE on success + */ +error_t pcf85063_get_datetime(struct Device* device, struct RtcDateTime* dt); + +/** + * Write the date and time to the RTC. + * @param[in] device pcf85063 device + * @param[in] dt Pointer to RtcDateTime to write (year must be 2000–2199) + * @return ERROR_NONE on success, ERROR_INVALID_ARGUMENT if any field is out of range + */ +error_t pcf85063_set_datetime(struct Device* device, const struct RtcDateTime* dt); + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/pcf85063-module/include/pcf85063_module.h b/Drivers/pcf85063-module/include/pcf85063_module.h new file mode 100644 index 000000000..c933b3423 --- /dev/null +++ b/Drivers/pcf85063-module/include/pcf85063_module.h @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +extern struct Module pcf85063_module; + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/pcf85063-module/module.yaml b/Drivers/pcf85063-module/module.yaml new file mode 100644 index 000000000..a07d6f334 --- /dev/null +++ b/Drivers/pcf85063-module/module.yaml @@ -0,0 +1,3 @@ +dependencies: + - TactilityKernel +bindings: bindings diff --git a/Drivers/pcf85063-module/source/module.cpp b/Drivers/pcf85063-module/source/module.cpp new file mode 100644 index 000000000..a9a7b53a3 --- /dev/null +++ b/Drivers/pcf85063-module/source/module.cpp @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include + +extern "C" { + +extern Driver pcf85063_driver; + +static Driver* const pcf85063_drivers[] = { + &pcf85063_driver, + nullptr +}; + +Module pcf85063_module = { + .name = "pcf85063", + .start = nullptr, + .stop = nullptr, + .drivers = pcf85063_drivers, + .symbols = nullptr, + .internal = nullptr +}; + +} diff --git a/Drivers/pcf85063-module/source/pcf85063.cpp b/Drivers/pcf85063-module/source/pcf85063.cpp new file mode 100644 index 000000000..77ab998c8 --- /dev/null +++ b/Drivers/pcf85063-module/source/pcf85063.cpp @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include +#include +#include + +#define TAG "PCF85063" + +static constexpr uint8_t REG_CTRL1 = 0x00; // Control_1 +static constexpr uint8_t CTRL1_STOP = 0x20; // STOP: 1 = oscillator stopped +static constexpr uint8_t REG_SECONDS = 0x04; // Seconds BCD, bit 7 = OS (clock integrity) +// Registers 0x04–0x0A: seconds, minutes, hours, days, weekdays, months, years + +static constexpr TickType_t I2C_TIMEOUT_TICKS = pdMS_TO_TICKS(50); + +#define GET_CONFIG(device) (static_cast((device)->config)) + +// region Helpers + +static uint8_t bcd_to_dec(uint8_t bcd) { return static_cast((bcd >> 4) * 10 + (bcd & 0x0F)); } +static uint8_t dec_to_bcd(uint8_t dec) { return static_cast(((dec / 10) << 4) | (dec % 10)); } + +// endregion + +// region Driver lifecycle + +static error_t start(Device* device) { + auto* i2c_controller = device_get_parent(device); + if (device_get_type(i2c_controller) != &I2C_CONTROLLER_TYPE) { + LOG_E(TAG, "Parent is not an I2C controller"); + return ERROR_RESOURCE; + } + + auto address = GET_CONFIG(device)->address; + + // Clear the STOP bit only, so the oscillator runs while the other Control_1 settings + // (e.g. the crystal load-capacitance select) keep whatever the board already configured. + if (i2c_controller_register8_reset_bits(i2c_controller, address, REG_CTRL1, CTRL1_STOP, I2C_TIMEOUT_TICKS) != ERROR_NONE) { + LOG_E(TAG, "Failed to clear STOP bit at 0x%02X", address); + return ERROR_RESOURCE; + } + + return ERROR_NONE; +} + +static error_t stop(Device* device) { + // The RTC keeps running on its backup supply; nothing to do. + return ERROR_NONE; +} + +// endregion + +extern "C" { + +error_t pcf85063_get_datetime(Device* device, RtcDateTime* dt) { + auto* i2c_controller = device_get_parent(device); + auto address = GET_CONFIG(device)->address; + + // Burst-read 7 registers starting at 0x04: + // [0]=seconds [1]=minutes [2]=hours [3]=days [4]=weekdays [5]=months [6]=years + uint8_t buf[7] = {}; + error_t error = i2c_controller_read_register(i2c_controller, address, REG_SECONDS, buf, sizeof(buf), I2C_TIMEOUT_TICKS); + if (error != ERROR_NONE) return error; + + if (buf[0] & 0x80u) { + LOG_E(TAG, "Clock integrity compromised (OS flag set) — data unreliable"); + return ERROR_INVALID_STATE; + } + dt->second = bcd_to_dec(buf[0] & 0x7Fu); // mask OS flag + dt->minute = bcd_to_dec(buf[1] & 0x7Fu); + dt->hour = bcd_to_dec(buf[2] & 0x3Fu); + dt->day = bcd_to_dec(buf[3] & 0x3Fu); + // buf[4] = weekday — ignored + dt->month = bcd_to_dec(buf[5] & 0x1Fu); + bool century = (buf[5] & 0x80u) != 0; + dt->year = static_cast(2000 + bcd_to_dec(buf[6]) + (century ? 100 : 0)); + + return ERROR_NONE; +} + +error_t pcf85063_set_datetime(Device* device, const RtcDateTime* dt) { + if (dt->year < 2000 || dt->year > 2199 || + dt->month < 1 || dt->month > 12 || + dt->day < 1 || dt->day > 31 || + dt->hour > 23 || dt->minute > 59 || dt->second > 59) { + return ERROR_INVALID_ARGUMENT; + } + + auto* i2c_controller = device_get_parent(device); + auto address = GET_CONFIG(device)->address; + + bool century = (dt->year >= 2100); + uint8_t y = static_cast(century ? dt->year - 2100 : dt->year - 2000); + + uint8_t buf[7] = {}; + buf[0] = dec_to_bcd(dt->second); + buf[1] = dec_to_bcd(dt->minute); + buf[2] = dec_to_bcd(dt->hour); + buf[3] = dec_to_bcd(dt->day); + buf[4] = 0; // weekday — leave as Sunday (unused) + buf[5] = static_cast(dec_to_bcd(dt->month) | (century ? 0x80u : 0x00u)); + buf[6] = dec_to_bcd(y); + + return i2c_controller_write_register(i2c_controller, address, REG_SECONDS, buf, sizeof(buf), I2C_TIMEOUT_TICKS); +} + +RtcApi pcf85063_rtc_api = { + .get_time = pcf85063_get_datetime, + .set_time = pcf85063_set_datetime +}; + +Driver pcf85063_driver = { + .name = "pcf85063", + .compatible = (const char*[]) { "nxp,pcf85063a", nullptr }, + .start_device = start, + .stop_device = stop, + .probe = nullptr, + .api = &pcf85063_rtc_api, + .device_type = &RTC_TYPE, + .internal = nullptr +}; + +} // extern "C" diff --git a/Drivers/sh8601-module/source/sh8601.cpp b/Drivers/sh8601-module/source/sh8601.cpp index 4a40d12d4..249a5f1f4 100644 --- a/Drivers/sh8601-module/source/sh8601.cpp +++ b/Drivers/sh8601-module/source/sh8601.cpp @@ -178,11 +178,12 @@ static error_t start(Device* device) { .octal_mode = 0, .quad_mode = 1, .sio_mode = 0, - // The LVGL color buffer lives in PSRAM (see PREFER_EXTERNAL_RAM). Without this the SPI - // driver bounces every chunk through a freshly-allocated internal DMA buffer (up to the - // bus's 64KB max-transfer-size), which fails with "Failed to allocate priv TX buffer" - // once internal RAM is tight (e.g. during Wi-Fi). ESP32-S3 can DMA from PSRAM directly. - .psram_dma_direct = 1, + // The LVGL color buffer lives in PSRAM (see PREFER_EXTERNAL_RAM), but ESP32-S3 cannot + // DMA from PSRAM reliably: under PSRAM contention (e.g. Wi-Fi) the QSPI DMA underruns, + // the SPI driver flags the transfer as failed and the panel IO's transaction pool + // desyncs permanently, so the display stops updating. Bounce each chunk through an + // internal DMA buffer instead - the bus max-transfer-size keeps those buffers small. + .psram_dma_direct = 0, .lsb_first = 0, .cs_high_active = 0, },