From aa74a4bb74c7222bc45c44667d4fbcb84e4641d2 Mon Sep 17 00:00:00 2001 From: Loelin Date: Sun, 25 Oct 2026 11:33:51 -0400 Subject: [PATCH 1/4] Waveshare S3 Touch AMOLED 1.8": add device support --- .../CMakeLists.txt | 19 ++ .../device.properties | 30 +++ .../waveshare-s3-touch-amoled-18/module.yaml | 11 + .../source/bluetooth_reconnect.cpp | 225 ++++++++++++++++++ .../source/bluetooth_reconnect.h | 14 ++ .../source/expander_reset.cpp | 86 +++++++ .../source/lvgl_insets.cpp | 48 ++++ .../source/lvgl_scaling.cpp | 86 +++++++ .../source/module.cpp | 32 +++ .../waveshare,s3-touch-amoled-18.dts | 155 ++++++++++++ 10 files changed, 706 insertions(+) create mode 100644 Devices/waveshare-s3-touch-amoled-18/CMakeLists.txt create mode 100644 Devices/waveshare-s3-touch-amoled-18/device.properties create mode 100644 Devices/waveshare-s3-touch-amoled-18/module.yaml create mode 100644 Devices/waveshare-s3-touch-amoled-18/source/bluetooth_reconnect.cpp create mode 100644 Devices/waveshare-s3-touch-amoled-18/source/bluetooth_reconnect.h create mode 100644 Devices/waveshare-s3-touch-amoled-18/source/expander_reset.cpp create mode 100644 Devices/waveshare-s3-touch-amoled-18/source/lvgl_insets.cpp create mode 100644 Devices/waveshare-s3-touch-amoled-18/source/lvgl_scaling.cpp create mode 100644 Devices/waveshare-s3-touch-amoled-18/source/module.cpp create mode 100644 Devices/waveshare-s3-touch-amoled-18/waveshare,s3-touch-amoled-18.dts 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/bluetooth_reconnect.cpp b/Devices/waveshare-s3-touch-amoled-18/source/bluetooth_reconnect.cpp new file mode 100644 index 000000000..a6f592bbc --- /dev/null +++ b/Devices/waveshare-s3-touch-amoled-18/source/bluetooth_reconnect.cpp @@ -0,0 +1,225 @@ +// SPDX-License-Identifier: Apache-2.0 + +// Board-local Bluetooth auto-reconnect for the Waveshare S3 Touch AMOLED 1.8. +// +// Tactility's own auto-connect runs on the "bt-events" thread +// (Tactility/Source/bluetooth/Bluetooth.cpp). That thread can be blocked while the radio is on, +// so saved peers (e.g. a BLE keyboard) never get reconnected. This task subscribes to the kernel +// BT driver directly - its own subscription and its own thread, so it doesn't depend on bt-events +// - and, whenever the radio is on, scans for and reconnects every saved peer it sees. + +#include + +#if defined(CONFIG_BT_NIMBLE_ENABLED) + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace tt::bluetooth { + +// The core HID host resolves a peer's address type from a scan cache that is filled in by the +// (blocked) bt-events thread. The helper that fills it is internal to Tactility (declared in +// Private/Tactility/bluetooth/BluetoothPrivate.h, which isn't exported to components), so it is +// re-declared here to prime that cache with the address type this task learns from its own scan. +void cacheScanAddr(const uint8_t addr[6], uint8_t addr_type); + +} // namespace tt::bluetooth + +namespace { + +constexpr auto* TAG = "WsBtReconnect"; + +// How long the task blocks per wait, and how many of those slices make up one periodic retry. +// The slice bounds how quickly stop() is noticed; the total paces the retry scan. +constexpr TickType_t WAIT_SLICE = pdMS_TO_TICKS(500); +constexpr int RETRY_SLICES = 40; // ~20 s +// This task runs the same heavy work the core dispatches to the main task (settings::loadAll() +// plus tt::bluetooth::connect() -> hidHostConnect()), so it needs at least the main task's stack +// (CONFIG_ESP_MAIN_TASK_STACK_SIZE, 6144). Sized to match NimBLE's host task. +constexpr size_t TASK_STACK_SIZE = 8192; + +// The advertised name must fit the 31-byte legacy advertising payload (<= 26 chars alongside the +// advertising flags). "S3 Touch AMOLED 1.8" fits, but it is pinned here so a change to the (longer) +// UI device name can't silently break advertising. +constexpr const char* ADVERTISED_NAME = "S3 Touch AMOLED 1.8"; + +std::atomic reconnect_running { false }; +std::unique_ptr reconnect_thread; + +struct SavedPeer { + std::array addr; + int profile_id; + bool attempted; +}; + +// Only ever touched by the task. +std::vector saved_peers; +Device* bt_device = nullptr; + +void refresh_saved_peers() { + saved_peers.clear(); + for (const auto& peer : tt::bluetooth::settings::loadAll()) { + saved_peers.push_back(SavedPeer { .addr = peer.addr, .profile_id = peer.profileId, .attempted = false }); + } +} + +SavedPeer* find_saved_peer(const BtAddr addr) { + for (auto& peer : saved_peers) { + if (std::memcmp(peer.addr.data(), addr, 6) == 0) { + return &peer; + } + } + return nullptr; +} + +void connect_peer(const BtPeerRecord& record) { + SavedPeer* peer = find_saved_peer(record.addr); + if (peer == nullptr || peer->attempted) { + return; + } + peer->attempted = true; + + // Prime the core HID host cache with the address type from our own scan. + tt::bluetooth::cacheScanAddr(record.addr, record.addr_type); + + const auto hex = tt::bluetooth::settings::addrToHex(peer->addr); + LOG_I(TAG, "Reconnecting saved peer %s (profile=%d)", hex.c_str(), peer->profile_id); + tt::bluetooth::connect(peer->addr, peer->profile_id); +} + +void scan_if_idle() { + if (bt_device == nullptr || saved_peers.empty()) { + return; + } + if (!tt::bluetooth::isRadioOnOrPending(bt_device)) { + return; + } + if (bluetooth_is_scanning(bt_device)) { + return; + } + for (auto& peer : saved_peers) { + peer.attempted = false; + } + bluetooth_scan_start(bt_device); +} + +int32_t task_main() { + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + + // Wait for the Bluetooth device to be constructed and started (the module starts before the + // devicetree's devices do). + while (reconnect_running.load()) { + if (device_get_first_active_by_type(&BLUETOOTH_TYPE, &bt_device) == ERROR_NONE) { + break; + } + bt_device = nullptr; + vTaskDelay(pdMS_TO_TICKS(500)); + } + + BtEventSubscription subscription {}; + if (!reconnect_running.load() || bt_device == nullptr || + bluetooth_event_subscribe(bt_device, &subscription, &event_group) != ERROR_NONE) { + if (bt_device != nullptr) { + device_put(bt_device); + bt_device = nullptr; + } + task_event_group_destruct(&event_group); + return 0; + } + + // Advertise a name that fits the legacy advertising payload (see ADVERTISED_NAME). The driver + // restarts advertising with it if the radio is already on. + if (bluetooth_set_device_name(bt_device, ADVERTISED_NAME) != ERROR_NONE) { + LOG_W(TAG, "Failed to set the advertised Bluetooth name"); + } + + refresh_saved_peers(); + scan_if_idle(); + + int slices = 0; + while (reconnect_running.load()) { + task_event_group_wait_any(&event_group, nullptr, WAIT_SLICE); + + BtEvent event {}; + while (bluetooth_event_poll(&subscription, &event) == ERROR_NONE) { + switch (event.type) { + case BT_EVENT_RADIO_STATE_CHANGED: + if (event.radio_state == BT_RADIO_STATE_ON) { + refresh_saved_peers(); + scan_if_idle(); + } + break; + case BT_EVENT_SCAN_FINISHED: + LOG_I(TAG, "Scan finished"); + break; + case BT_EVENT_PEER_FOUND: + connect_peer(event.peer); + break; + default: + break; + } + } + + // Periodic retry: pick up newly paired peers and rescan when nothing connected yet. + if (++slices >= RETRY_SLICES) { + slices = 0; + refresh_saved_peers(); + scan_if_idle(); + } + } + + bluetooth_event_unsubscribe(bt_device, &subscription); + device_put(bt_device); + bt_device = nullptr; + task_event_group_destruct(&event_group); + return 0; +} + +void task_start() { + reconnect_thread = std::make_unique( + "ws-bt-reconnect", + TASK_STACK_SIZE, + []() -> int32_t { return task_main(); } + ); + reconnect_thread->start(); +} + +} // namespace + +void waveshare_bluetooth_reconnect_start() { + if (reconnect_running.exchange(true)) { + return; + } + task_start(); +} + +void waveshare_bluetooth_reconnect_stop() { + if (!reconnect_running.exchange(false)) { + return; + } + if (reconnect_thread != nullptr) { + reconnect_thread->join(); + reconnect_thread.reset(); + } +} + +#else + +void waveshare_bluetooth_reconnect_start() {} +void waveshare_bluetooth_reconnect_stop() {} + +#endif // CONFIG_BT_NIMBLE_ENABLED diff --git a/Devices/waveshare-s3-touch-amoled-18/source/bluetooth_reconnect.h b/Devices/waveshare-s3-touch-amoled-18/source/bluetooth_reconnect.h new file mode 100644 index 000000000..bcd5e3a6b --- /dev/null +++ b/Devices/waveshare-s3-touch-amoled-18/source/bluetooth_reconnect.h @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +/** + * Board-local Bluetooth auto-reconnect for the Waveshare S3 Touch AMOLED 1.8. + * + * Starts a task that scans and reconnects saved (paired) BLE peers. It runs its own kernel BT + * event subscription on its own thread instead of relying on Tactility's "bt-events" thread, + * which can block while the radio is on. + */ +void waveshare_bluetooth_reconnect_start(); + +/** Stops the task, if running. */ +void waveshare_bluetooth_reconnect_stop(); 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..230d28ab3 --- /dev/null +++ b/Devices/waveshare-s3-touch-amoled-18/source/module.cpp @@ -0,0 +1,32 @@ +#include "bluetooth_reconnect.h" + +#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(); + waveshare_bluetooth_reconnect_start(); + return ERROR_NONE; +} + +static error_t stop() { + waveshare_bluetooth_reconnect_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"; + }; + }; +}; From be6d8f0e029933d3724b60484df770ab356fe442 Mon Sep 17 00:00:00 2001 From: Loelin Date: Sun, 25 Oct 2026 11:33:53 -0400 Subject: [PATCH 2/4] Add PCF85063A RTC driver; declare it on the 1.8 board --- Drivers/pcf85063-module/CMakeLists.txt | 11 + Drivers/pcf85063-module/LICENSE-Apache-2.0.md | 195 ++++++++++++++++++ Drivers/pcf85063-module/README.md | 12 ++ .../bindings/nxp,pcf85063a.yaml | 5 + .../include/bindings/pcf85063.h | 15 ++ .../include/drivers/pcf85063.h | 37 ++++ .../pcf85063-module/include/pcf85063_module.h | 14 ++ Drivers/pcf85063-module/module.yaml | 3 + Drivers/pcf85063-module/source/module.cpp | 23 +++ Drivers/pcf85063-module/source/pcf85063.cpp | 124 +++++++++++ 10 files changed, 439 insertions(+) create mode 100644 Drivers/pcf85063-module/CMakeLists.txt create mode 100644 Drivers/pcf85063-module/LICENSE-Apache-2.0.md create mode 100644 Drivers/pcf85063-module/README.md create mode 100644 Drivers/pcf85063-module/bindings/nxp,pcf85063a.yaml create mode 100644 Drivers/pcf85063-module/include/bindings/pcf85063.h create mode 100644 Drivers/pcf85063-module/include/drivers/pcf85063.h create mode 100644 Drivers/pcf85063-module/include/pcf85063_module.h create mode 100644 Drivers/pcf85063-module/module.yaml create mode 100644 Drivers/pcf85063-module/source/module.cpp create mode 100644 Drivers/pcf85063-module/source/pcf85063.cpp 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" From 4865428e00534ed6f0ceda0317cf78ce182c40ee Mon Sep 17 00:00:00 2001 From: Loelin Date: Sun, 25 Oct 2026 11:34:03 -0400 Subject: [PATCH 3/4] SH8601/FT5x06: internal DMA buffers and bounded I2C timeouts --- Drivers/ft5x06-module/source/ft5x06.cpp | 15 +++++++++++++-- Drivers/sh8601-module/source/sh8601.cpp | 11 ++++++----- 2 files changed, 19 insertions(+), 7 deletions(-) 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/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, }, From 72feb1cc2fea79a95656df5f2761e412269c83c2 Mon Sep 17 00:00:00 2001 From: Loelin Date: Sun, 25 Oct 2026 11:36:01 -0400 Subject: [PATCH 4/4] 1.8 board: drop board-local Bluetooth reconnect (handled by the service after #710) --- .../source/bluetooth_reconnect.cpp | 225 ------------------ .../source/bluetooth_reconnect.h | 14 -- .../source/module.cpp | 4 - 3 files changed, 243 deletions(-) delete mode 100644 Devices/waveshare-s3-touch-amoled-18/source/bluetooth_reconnect.cpp delete mode 100644 Devices/waveshare-s3-touch-amoled-18/source/bluetooth_reconnect.h diff --git a/Devices/waveshare-s3-touch-amoled-18/source/bluetooth_reconnect.cpp b/Devices/waveshare-s3-touch-amoled-18/source/bluetooth_reconnect.cpp deleted file mode 100644 index a6f592bbc..000000000 --- a/Devices/waveshare-s3-touch-amoled-18/source/bluetooth_reconnect.cpp +++ /dev/null @@ -1,225 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -// Board-local Bluetooth auto-reconnect for the Waveshare S3 Touch AMOLED 1.8. -// -// Tactility's own auto-connect runs on the "bt-events" thread -// (Tactility/Source/bluetooth/Bluetooth.cpp). That thread can be blocked while the radio is on, -// so saved peers (e.g. a BLE keyboard) never get reconnected. This task subscribes to the kernel -// BT driver directly - its own subscription and its own thread, so it doesn't depend on bt-events -// - and, whenever the radio is on, scans for and reconnects every saved peer it sees. - -#include - -#if defined(CONFIG_BT_NIMBLE_ENABLED) - -#include -#include -#include - -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -namespace tt::bluetooth { - -// The core HID host resolves a peer's address type from a scan cache that is filled in by the -// (blocked) bt-events thread. The helper that fills it is internal to Tactility (declared in -// Private/Tactility/bluetooth/BluetoothPrivate.h, which isn't exported to components), so it is -// re-declared here to prime that cache with the address type this task learns from its own scan. -void cacheScanAddr(const uint8_t addr[6], uint8_t addr_type); - -} // namespace tt::bluetooth - -namespace { - -constexpr auto* TAG = "WsBtReconnect"; - -// How long the task blocks per wait, and how many of those slices make up one periodic retry. -// The slice bounds how quickly stop() is noticed; the total paces the retry scan. -constexpr TickType_t WAIT_SLICE = pdMS_TO_TICKS(500); -constexpr int RETRY_SLICES = 40; // ~20 s -// This task runs the same heavy work the core dispatches to the main task (settings::loadAll() -// plus tt::bluetooth::connect() -> hidHostConnect()), so it needs at least the main task's stack -// (CONFIG_ESP_MAIN_TASK_STACK_SIZE, 6144). Sized to match NimBLE's host task. -constexpr size_t TASK_STACK_SIZE = 8192; - -// The advertised name must fit the 31-byte legacy advertising payload (<= 26 chars alongside the -// advertising flags). "S3 Touch AMOLED 1.8" fits, but it is pinned here so a change to the (longer) -// UI device name can't silently break advertising. -constexpr const char* ADVERTISED_NAME = "S3 Touch AMOLED 1.8"; - -std::atomic reconnect_running { false }; -std::unique_ptr reconnect_thread; - -struct SavedPeer { - std::array addr; - int profile_id; - bool attempted; -}; - -// Only ever touched by the task. -std::vector saved_peers; -Device* bt_device = nullptr; - -void refresh_saved_peers() { - saved_peers.clear(); - for (const auto& peer : tt::bluetooth::settings::loadAll()) { - saved_peers.push_back(SavedPeer { .addr = peer.addr, .profile_id = peer.profileId, .attempted = false }); - } -} - -SavedPeer* find_saved_peer(const BtAddr addr) { - for (auto& peer : saved_peers) { - if (std::memcmp(peer.addr.data(), addr, 6) == 0) { - return &peer; - } - } - return nullptr; -} - -void connect_peer(const BtPeerRecord& record) { - SavedPeer* peer = find_saved_peer(record.addr); - if (peer == nullptr || peer->attempted) { - return; - } - peer->attempted = true; - - // Prime the core HID host cache with the address type from our own scan. - tt::bluetooth::cacheScanAddr(record.addr, record.addr_type); - - const auto hex = tt::bluetooth::settings::addrToHex(peer->addr); - LOG_I(TAG, "Reconnecting saved peer %s (profile=%d)", hex.c_str(), peer->profile_id); - tt::bluetooth::connect(peer->addr, peer->profile_id); -} - -void scan_if_idle() { - if (bt_device == nullptr || saved_peers.empty()) { - return; - } - if (!tt::bluetooth::isRadioOnOrPending(bt_device)) { - return; - } - if (bluetooth_is_scanning(bt_device)) { - return; - } - for (auto& peer : saved_peers) { - peer.attempted = false; - } - bluetooth_scan_start(bt_device); -} - -int32_t task_main() { - TaskEventGroup event_group {}; - task_event_group_construct(&event_group); - - // Wait for the Bluetooth device to be constructed and started (the module starts before the - // devicetree's devices do). - while (reconnect_running.load()) { - if (device_get_first_active_by_type(&BLUETOOTH_TYPE, &bt_device) == ERROR_NONE) { - break; - } - bt_device = nullptr; - vTaskDelay(pdMS_TO_TICKS(500)); - } - - BtEventSubscription subscription {}; - if (!reconnect_running.load() || bt_device == nullptr || - bluetooth_event_subscribe(bt_device, &subscription, &event_group) != ERROR_NONE) { - if (bt_device != nullptr) { - device_put(bt_device); - bt_device = nullptr; - } - task_event_group_destruct(&event_group); - return 0; - } - - // Advertise a name that fits the legacy advertising payload (see ADVERTISED_NAME). The driver - // restarts advertising with it if the radio is already on. - if (bluetooth_set_device_name(bt_device, ADVERTISED_NAME) != ERROR_NONE) { - LOG_W(TAG, "Failed to set the advertised Bluetooth name"); - } - - refresh_saved_peers(); - scan_if_idle(); - - int slices = 0; - while (reconnect_running.load()) { - task_event_group_wait_any(&event_group, nullptr, WAIT_SLICE); - - BtEvent event {}; - while (bluetooth_event_poll(&subscription, &event) == ERROR_NONE) { - switch (event.type) { - case BT_EVENT_RADIO_STATE_CHANGED: - if (event.radio_state == BT_RADIO_STATE_ON) { - refresh_saved_peers(); - scan_if_idle(); - } - break; - case BT_EVENT_SCAN_FINISHED: - LOG_I(TAG, "Scan finished"); - break; - case BT_EVENT_PEER_FOUND: - connect_peer(event.peer); - break; - default: - break; - } - } - - // Periodic retry: pick up newly paired peers and rescan when nothing connected yet. - if (++slices >= RETRY_SLICES) { - slices = 0; - refresh_saved_peers(); - scan_if_idle(); - } - } - - bluetooth_event_unsubscribe(bt_device, &subscription); - device_put(bt_device); - bt_device = nullptr; - task_event_group_destruct(&event_group); - return 0; -} - -void task_start() { - reconnect_thread = std::make_unique( - "ws-bt-reconnect", - TASK_STACK_SIZE, - []() -> int32_t { return task_main(); } - ); - reconnect_thread->start(); -} - -} // namespace - -void waveshare_bluetooth_reconnect_start() { - if (reconnect_running.exchange(true)) { - return; - } - task_start(); -} - -void waveshare_bluetooth_reconnect_stop() { - if (!reconnect_running.exchange(false)) { - return; - } - if (reconnect_thread != nullptr) { - reconnect_thread->join(); - reconnect_thread.reset(); - } -} - -#else - -void waveshare_bluetooth_reconnect_start() {} -void waveshare_bluetooth_reconnect_stop() {} - -#endif // CONFIG_BT_NIMBLE_ENABLED diff --git a/Devices/waveshare-s3-touch-amoled-18/source/bluetooth_reconnect.h b/Devices/waveshare-s3-touch-amoled-18/source/bluetooth_reconnect.h deleted file mode 100644 index bcd5e3a6b..000000000 --- a/Devices/waveshare-s3-touch-amoled-18/source/bluetooth_reconnect.h +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#pragma once - -/** - * Board-local Bluetooth auto-reconnect for the Waveshare S3 Touch AMOLED 1.8. - * - * Starts a task that scans and reconnects saved (paired) BLE peers. It runs its own kernel BT - * event subscription on its own thread instead of relying on Tactility's "bt-events" thread, - * which can block while the radio is on. - */ -void waveshare_bluetooth_reconnect_start(); - -/** Stops the task, if running. */ -void waveshare_bluetooth_reconnect_stop(); diff --git a/Devices/waveshare-s3-touch-amoled-18/source/module.cpp b/Devices/waveshare-s3-touch-amoled-18/source/module.cpp index 230d28ab3..4a25a2359 100644 --- a/Devices/waveshare-s3-touch-amoled-18/source/module.cpp +++ b/Devices/waveshare-s3-touch-amoled-18/source/module.cpp @@ -1,5 +1,3 @@ -#include "bluetooth_reconnect.h" - #include // Defined in expander_reset.cpp (C++ linkage). @@ -11,12 +9,10 @@ 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(); - waveshare_bluetooth_reconnect_start(); return ERROR_NONE; } static error_t stop() { - waveshare_bluetooth_reconnect_stop(); return ERROR_NONE; }