From 17688b05affccd2f1af329c481ff686863dd357a Mon Sep 17 00:00:00 2001 From: Niels Timmer Date: Sun, 19 Apr 2026 14:00:49 +0200 Subject: [PATCH 01/15] Add JC3248W535 board scaffolding: env, layout, touch (driver WIP) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lays down everything needed for a 320x480 Guition JC3248W535 build except the AXS15231B QSPI panel driver itself, which is not in mainline LovyanGFX (issue #699) and will follow in a separate commit. - platformio.ini: new [env:jc3248w535] for ESP32-S3-N16R8 with QSPI/PSRAM memory config and 16MB partitions. Pins pre-wired from Guition reference and three independent community drivers (me-processware, byte-me404, ESPhome-JC3248W535EN). Touch RST/INT omitted — conflicting pin info across sources, the controller works fine polled. - partitions_16mb.csv: dual 6.25MB OTA slots for the 16MB flash. - include/layout.h: dispatch to layout_320x480.h when DISPLAY_320x480. - include/layout_320x480.h: redesigned layout for the bigger 3.5" screen with larger gauges, always-visible AMS strip, and a more generous ETA/bottom zone. Not a stretch of the 240x320 layout. - src/button.cpp: USE_AXS_TOUCH branch for the AXS15231B integrated touch controller. I2C @0x3B, 11-byte read-touchpad command packet returning 14 bytes; protocol documented in the me-processware driver. - src/display_ui.cpp: BOARD_IS_JC3248W535 placeholder with an explicit #error so the env is declared but cannot yet build. Panel driver lands in a follow-up. Existing boards unaffected: verified esp32s3 env still builds cleanly. --- include/layout.h | 4 +- include/layout_320x480.h | 134 +++++++++++++++++++++++++++++++++++++++ partitions_16mb.csv | 11 ++++ platformio.ini | 47 +++++++++++++- src/button.cpp | 57 +++++++++++++++++ src/display_ui.cpp | 11 +++- 6 files changed, 261 insertions(+), 3 deletions(-) create mode 100644 include/layout_320x480.h create mode 100644 partitions_16mb.csv diff --git a/include/layout.h b/include/layout.h index 6d1bf39..ae8fed2 100644 --- a/include/layout.h +++ b/include/layout.h @@ -6,7 +6,9 @@ // gauge positions, text positions, etc. // To add a new display: create layout_xxx.h and add an #elif here. -#if defined(DISPLAY_240x320) +#if defined(DISPLAY_320x480) + #include "layout_320x480.h" // 320x480 portrait (Guition JC3248W535) +#elif defined(DISPLAY_240x320) #include "layout_240x320.h" // 240x320 portrait (CYD, Waveshare) #else #include "layout_default.h" // ESP32-S3 Mini: ST7789 240x240 diff --git a/include/layout_320x480.h b/include/layout_320x480.h new file mode 100644 index 0000000..023a73b --- /dev/null +++ b/include/layout_320x480.h @@ -0,0 +1,134 @@ +#ifndef LAYOUT_320x480_H +#define LAYOUT_320x480_H + +// Layout profile: 320x480 portrait (Guition JC3248W535, AXS15231B QSPI IPS). +// Redesigned layout that uses the extra screen real estate — does not simply +// stretch the 240x320 layout. Gauges are larger, AMS strip is always visible, +// and the ETA / bottom status areas are generously sized. + +// --- Screen dimensions --- +#define LY_W 320 +#define LY_H 480 + +// --- LED progress bar (top, y=0) --- +#define LY_BAR_W 316 +#define LY_BAR_H 7 + +// --- Header bar --- +#define LY_HDR_Y 10 +#define LY_HDR_H 26 +#define LY_HDR_NAME_X 8 +#define LY_HDR_CY 23 // vertical center of header text +#define LY_HDR_BADGE_RX 10 // badge right margin from SCREEN_W +#define LY_HDR_DOT_CY 13 // multi-printer indicator dot Y + +// --- Printing: 2x3 gauge grid (3 columns, 2 rows) --- +// 320px wide split into 3 columns of ~107px — gauges are r=48, spacing tuned +// so left/right edges sit ~5px from the screen edges. +#define LY_GAUGE_R 48 // radius for all gauges (was 32 on 240x) +#define LY_GAUGE_T 9 // progress arc thickness (was 6 on 240x) +#define LY_COL1 56 +#define LY_COL2 160 +#define LY_COL3 264 +#define LY_ROW1 92 // top row center Y (gauge top edge y=44) +#define LY_ROW2 228 // bottom row center Y (gauge top edge y=180) + +// --- AMS tray visualization zone (below gauge grid) --- +// Row 2 gauges bottom edge is at y=276 (228+48). Labels extend ~12px below, +// so AMS starts at y=295 with 4px gap under it (ETA begins at y=355). +#define LY_AMS_Y 295 +#define LY_AMS_H 56 +#define LY_AMS_BAR_H 32 +#define LY_AMS_BAR_GAP 3 +#define LY_AMS_GROUP_GAP 10 +#define LY_AMS_LABEL_OFFY 4 +#define LY_AMS_MARGIN 10 +#define LY_AMS_BAR_MAX_W 42 + +// --- Printing: ETA / info zone --- +#define LY_ETA_Y 360 +#define LY_ETA_H 46 +#define LY_ETA_TEXT_Y 383 + +// --- Printing: bottom status bar --- +#define LY_BOT_Y 414 +#define LY_BOT_H 26 +#define LY_BOT_CY 427 + +// --- Printing: WiFi signal indicator --- +#define LY_WIFI_X 6 +#define LY_WIFI_Y 452 + +// --- Idle screen (with printer) --- +#define LY_IDLE_NAME_Y 45 +#define LY_IDLE_STATE_Y 75 +#define LY_IDLE_STATE_H 28 +#define LY_IDLE_STATE_TY 89 +#define LY_IDLE_DOT_Y 125 +#define LY_IDLE_GAUGE_R 46 +#define LY_IDLE_GAUGE_Y 210 +#define LY_IDLE_G_OFFSET 80 + +// --- Idle screen: AMS zone (below gauges) --- +#define LY_IDLE_AMS_Y 275 +#define LY_IDLE_AMS_H 80 +#define LY_IDLE_AMS_BAR_H 46 + +// --- Idle screen (no printer) --- +#define LY_IDLE_NP_TITLE_Y 60 +#define LY_IDLE_NP_WIFI_Y 120 +#define LY_IDLE_NP_DOT_Y 150 +#define LY_IDLE_NP_MSG_Y 200 +#define LY_IDLE_NP_OPEN_Y 240 +#define LY_IDLE_NP_IP_Y 290 + +// --- Finished screen (portrait, vertically centered) --- +#define LY_FIN_GAUGE_R 48 +#define LY_FIN_GL 96 +#define LY_FIN_GR 224 +#define LY_FIN_GY 150 +#define LY_FIN_TEXT_Y 245 +#define LY_FIN_FILE_Y 290 +#define LY_FIN_KWH_Y 320 +#define LY_FIN_AMS_Y 345 +#define LY_FIN_AMS_H 65 +#define LY_FIN_AMS_BAR_H 38 +#define LY_FIN_BOT_Y 436 +#define LY_FIN_BOT_H 28 +#define LY_FIN_WIFI_Y 458 + +// --- AP mode screen --- +#define LY_AP_TITLE_Y 60 +#define LY_AP_SSID_LBL_Y 120 +#define LY_AP_SSID_Y 160 +#define LY_AP_PASS_LBL_Y 210 +#define LY_AP_PASS_Y 240 +#define LY_AP_OPEN_Y 280 +#define LY_AP_IP_Y 315 + +// --- Simple clock (centered in 480px height) --- +#define LY_CLK_CLEAR_Y 110 +#define LY_CLK_CLEAR_H 280 +#define LY_CLK_TIME_Y 210 +#define LY_CLK_AMPM_Y 265 +#define LY_CLK_DATE_Y 310 + +// --- Pong/Breakout clock (scaled for 320x480) --- +#define LY_ARK_BRICK_ROWS 5 +#define LY_ARK_COLS 10 +#define LY_ARK_BRICK_W 30 // 10 cols * 30 + 9 gaps * 2 = 318 (fits 320) +#define LY_ARK_BRICK_H 12 +#define LY_ARK_BRICK_GAP 2 +#define LY_ARK_START_X 1 +#define LY_ARK_START_Y 40 +#define LY_ARK_PADDLE_Y 460 +#define LY_ARK_PADDLE_W 44 +#define LY_ARK_TIME_Y 220 +#define LY_ARK_DATE_Y 10 +#define LY_ARK_DIGIT_W 42 +#define LY_ARK_DIGIT_H 64 +#define LY_ARK_COLON_W 16 +#define LY_ARK_DATE_CLR_X 50 +#define LY_ARK_DATE_CLR_W 220 + +#endif // LAYOUT_320x480_H diff --git a/partitions_16mb.csv b/partitions_16mb.csv new file mode 100644 index 0000000..bf19e7c --- /dev/null +++ b/partitions_16mb.csv @@ -0,0 +1,11 @@ +# Partition table for 16 MB flash (Guition JC3248W535 — ESP32-S3-N16R8) +# Dual OTA app slots of 6.25 MB each — large headroom for future features. +# SPIFFS reserved at end but unused (settings stored in NVS). +# NOTE: changing the partition table requires a full USB flash; OTA cannot update it. +# +# Name, Type, SubType, Offset, Size, +nvs, data, nvs, 0x9000, 0x5000, +otadata, data, ota, 0xe000, 0x2000, +app0, app, ota_0, 0x10000, 0x640000, +app1, app, ota_1, 0x650000, 0x640000, +spiffs, data, spiffs, 0xc90000, 0x360000, diff --git a/platformio.ini b/platformio.ini index 0e01132..50bbede 100644 --- a/platformio.ini +++ b/platformio.ini @@ -1,5 +1,6 @@ ; BambuHelper - Bambu Lab Printer Monitor -; Supports ESP32-S3 Super Mini, ESP32-2432S028 (CYD), ESP32-C3 Super Mini +; Supports ESP32-S3 Super Mini, ESP32-2432S028 (CYD), ESP32-C3 Super Mini, +; Waveshare ESP32-S3-Touch-LCD-2 / -1.54, Guition JC3248W535 ; --- Shared library dependencies --- [common] @@ -128,6 +129,50 @@ build_flags = -D CST816_RST=47 -D CST816_IRQ=48 +; ============================================================================= +; Guition JC3248W535 - 3.5" 320x480 IPS (AXS15231B QSPI + I2C touch) +; https://www.guition.com/ — ESP32-S3-N16R8, 16MB flash / 8MB PSRAM. +; Display driver is implemented locally (src/lgfx_panel_axs15231b.hpp) since +; mainline LovyanGFX does not ship Panel_AXS15231B. +; +; Touch RST/INT pins are not wired on the shipped Guition boards inspected — +; the touch IC shares reset with the display and is polled (no INT). If your +; board revision exposes these pins, add AXS_TOUCH_RST / AXS_TOUCH_IRQ here. +; ============================================================================= +[env:jc3248w535] +platform = espressif32 +board = esp32-s3-devkitc-1 +framework = arduino +monitor_speed = 115200 +board_build.partitions = partitions_16mb.csv +board_build.arduino.memory_type = qio_opi +board_upload.flash_size = 16MB +board_upload.maximum_size = 16777216 +board_build.f_flash = 80000000L +board_build.flash_mode = qio +lib_deps = ${common.lib_deps} +build_flags = + -D BOARD_VARIANT=\"jc3248w535\" + -D BOARD_IS_JC3248W535=1 + -D ENABLE_OTA_AUTO=1 + -D BOARD_HAS_PSRAM=1 + -D ARDUINO_USB_CDC_ON_BOOT=1 + ; --- Display (AXS15231B QSPI, 320x480 portrait native) --- + -D DISPLAY_320x480=1 + -D AXS_QSPI_HOST=1 ; SPI2_HOST + -D AXS_QSPI_FREQ=40000000 + -D AXS_QSPI_CS=45 + -D AXS_QSPI_SCK=47 + -D AXS_QSPI_D0=21 + -D AXS_QSPI_D1=48 + -D AXS_QSPI_D2=40 + -D AXS_QSPI_D3=39 + -D BACKLIGHT_PIN=1 + ; --- Touch (AXS15231B touch controller on I2C @0x3B) --- + -D USE_AXS_TOUCH=1 + -D AXS_TOUCH_SDA=4 + -D AXS_TOUCH_SCL=8 + ; ============================================================================= ; ESP32-C3 Super Mini + ST7789 240x240 ; ============================================================================= diff --git a/src/button.cpp b/src/button.cpp index 76066c9..8e7bfe3 100644 --- a/src/button.cpp +++ b/src/button.cpp @@ -27,6 +27,40 @@ value = Wire.read(); return true; } +#elif defined(USE_AXS_TOUCH) + // AXS15231B integrated touch controller. I2C slave at 0x3B. + // Reading a touch point requires writing an 11-byte command packet then + // reading 14 bytes back; status byte [1] & 0x0F holds touch count. + // Protocol documented in me-processware/JC3248W535-Driver. + #include + #define AXS_TOUCH_ADDR 0x3B + static const uint8_t AXS_READ_TOUCHPAD_CMD[11] = { + 0xb5, 0xab, 0xa5, 0x5a, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00 + }; + static bool axsTouchBusReady = false; + static bool axsTouchSeen = false; + + static bool axsTouchProbe() { + Wire.beginTransmission(AXS_TOUCH_ADDR); + return Wire.endTransmission(true) == 0; + } + + // Reads current touch state. Returns true if a touch is active and fills x/y + // with raw panel coordinates (native 320x480, caller applies any rotation). + static bool axsTouchRead(uint16_t& x, uint16_t& y) { + Wire.beginTransmission(AXS_TOUCH_ADDR); + Wire.write(AXS_READ_TOUCHPAD_CMD, sizeof(AXS_READ_TOUCHPAD_CMD)); + if (Wire.endTransmission() != 0) return false; + uint8_t buf[14] = {0}; + size_t got = Wire.requestFrom((uint8_t)AXS_TOUCH_ADDR, (uint8_t)14); + if (got < 6) return false; + for (size_t i = 0; i < got && i < sizeof(buf); ++i) buf[i] = Wire.read(); + uint8_t touches = buf[1] & 0x0F; + if (touches == 0) return false; + x = ((uint16_t)(buf[2] & 0x0F) << 8) | buf[3]; + y = ((uint16_t)(buf[4] & 0x0F) << 8) | buf[5]; + return true; + } #elif defined(TOUCH_CS) #include "display_ui.h" // extern tft for getTouch() #endif @@ -75,6 +109,21 @@ void initButton() { } return; } +#elif defined(USE_AXS_TOUCH) + if (buttonType == BTN_TOUCHSCREEN) { + Wire.begin(AXS_TOUCH_SDA, AXS_TOUCH_SCL); + Wire.setClock(400000); + axsTouchBusReady = true; + if (axsTouchProbe()) { + Serial.printf("AXS15231B touch initialized (I2C SDA=%d SCL=%d, addr 0x%02X)\n", + AXS_TOUCH_SDA, AXS_TOUCH_SCL, AXS_TOUCH_ADDR); + axsTouchSeen = true; + } else { + Serial.printf("AXS15231B touch did not answer at init (addr 0x%02X, SDA=%d SCL=%d); will keep retrying at runtime\n", + AXS_TOUCH_ADDR, AXS_TOUCH_SDA, AXS_TOUCH_SCL); + } + return; + } #endif if (buttonType == BTN_TOUCHSCREEN) return; if (buttonPin == 0) return; @@ -105,6 +154,14 @@ bool wasButtonPressed() { cst816Seen = true; } raw = (touchNum > 0); +#elif defined(USE_AXS_TOUCH) + if (!axsTouchBusReady) return false; + uint16_t tx = 0, ty = 0; + raw = axsTouchRead(tx, ty); + if (raw && !axsTouchSeen) { + Serial.printf("AXS15231B touch became responsive at runtime (addr 0x%02X)\n", AXS_TOUCH_ADDR); + axsTouchSeen = true; + } #elif defined(TOUCH_CS) uint16_t tx, ty; raw = tft.getTouch(&tx, &ty); diff --git a/src/display_ui.cpp b/src/display_ui.cpp index 972da39..df21cb8 100644 --- a/src/display_ui.cpp +++ b/src/display_ui.cpp @@ -203,6 +203,15 @@ class LGFX_WS154 : public lgfx::LGFX_Device { }; static LGFX_WS154 _tft_instance; +#elif defined(BOARD_IS_JC3248W535) +// --- Guition JC3248W535 + AXS15231B 320x480 (QSPI) -------------------------- +// Panel driver is not in mainline LovyanGFX (issue #699). A local +// Panel_AXS15231B + custom QSPI bus will be added in a follow-up commit +// (init sequence ported from moononournation/Arduino_GFX). For now the env +// is declared in platformio.ini and the layout / touch scaffolding is in +// place, but attempting to build this env will fail here with this message. + #error "JC3248W535 Panel_AXS15231B driver is not yet implemented — scaffolding only. Remove BOARD_IS_JC3248W535 to build other boards." + #elif defined(BOARD_IS_C3) // --- ESP32-C3 Super Mini + ST7789 240x280 ------------------------------------ class LGFX_C3 : public lgfx::LGFX_Device { @@ -244,7 +253,7 @@ class LGFX_C3 : public lgfx::LGFX_Device { static LGFX_C3 _tft_instance; #else - #error "No board variant defined. Add BOARD_IS_S3, DISPLAY_CYD, BOARD_IS_C3, BOARD_IS_WS200 or BOARD_IS_WS154 to build_flags." + #error "No board variant defined. Add BOARD_IS_S3, DISPLAY_CYD, BOARD_IS_C3, BOARD_IS_WS200, BOARD_IS_WS154 or BOARD_IS_JC3248W535 to build_flags." #endif // Global pointer + reference — accessed via `tft` throughout the codebase. From f786be771614cba3a04bfd08cbd181077151df95 Mon Sep 17 00:00:00 2001 From: Niels Timmer Date: Sun, 19 Apr 2026 14:08:13 +0200 Subject: [PATCH 02/15] Add Panel_AXS15231B driver for JC3248W535 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AXS15231B is a QSPI IPS controller with no driver in mainline LovyanGFX (issue #699). Implemented as a local header-only Panel_LCD subclass that reuses LovyanGFX's standard Bus_SPI — the "QSPI" on this part is purely protocol framing (commands wrapped in a 4-byte header, RAMWR prefixed with {0x32, 0x00, 0x2C, 0x00}) rather than true quad-data transfers, so single- wire MOSI on the D0 line is sufficient. - src/lgfx_panel_axs15231b.hpp: Panel_AXS15231B modeled on lgfx::Panel_NV3041A. Inlined init sequence ported from moononournation/Arduino_GFX's axs15231b_320480_type1_init_operations. Executes a software reset, walks a packed (cmd, arg_count, args…) table, then SLPOUT + DISPON with the datasheet-mandated delays. - src/display_ui.cpp: BOARD_IS_JC3248W535 branch now instantiates the real LGFX_JC3248W535 class (Bus_SPI @ 40 MHz, pin_dc=-1 since D/C is in-band). - platformio.ini: bump LovyanGFX from ^1.1.16 to ^1.2.19 — Panel_LCD base class and the QSPI panel reference implementations (NV3041A, SH8601Z) that this driver cribs from landed in the 1.2 line. Verified esp32s3 and cyd still build on the new version. Build: jc3248w535 compiles clean (1.3 MB firmware, 16% RAM, 20% of each OTA slot). Hardware test on a physical board is still TBD. --- platformio.ini | 2 +- src/display_ui.cpp | 57 ++++- src/lgfx_panel_axs15231b.hpp | 421 +++++++++++++++++++++++++++++++++++ 3 files changed, 472 insertions(+), 8 deletions(-) create mode 100644 src/lgfx_panel_axs15231b.hpp diff --git a/platformio.ini b/platformio.ini index 50bbede..11ff3f6 100644 --- a/platformio.ini +++ b/platformio.ini @@ -5,7 +5,7 @@ ; --- Shared library dependencies --- [common] lib_deps = - lovyan03/LovyanGFX@^1.1.16 + lovyan03/LovyanGFX@^1.2.19 knolleary/PubSubClient@^2.8 bblanchon/ArduinoJson@^7.0 diff --git a/src/display_ui.cpp b/src/display_ui.cpp index df21cb8..8db844c 100644 --- a/src/display_ui.cpp +++ b/src/display_ui.cpp @@ -204,13 +204,56 @@ class LGFX_WS154 : public lgfx::LGFX_Device { static LGFX_WS154 _tft_instance; #elif defined(BOARD_IS_JC3248W535) -// --- Guition JC3248W535 + AXS15231B 320x480 (QSPI) -------------------------- -// Panel driver is not in mainline LovyanGFX (issue #699). A local -// Panel_AXS15231B + custom QSPI bus will be added in a follow-up commit -// (init sequence ported from moononournation/Arduino_GFX). For now the env -// is declared in platformio.ini and the layout / touch scaffolding is in -// place, but attempting to build this env will fail here with this message. - #error "JC3248W535 Panel_AXS15231B driver is not yet implemented — scaffolding only. Remove BOARD_IS_JC3248W535 to build other boards." +// --- Guition JC3248W535 + AXS15231B 320x480 --------------------------------- +// Panel_AXS15231B is implemented locally (see lgfx_panel_axs15231b.hpp) since +// mainline LovyanGFX does not ship it. The controller uses QSPI framing on +// top of a regular single-wire SPI bus: every command is wrapped in a 4-byte +// header, and pixel data starts with {0x32, 0x00, 0x2C, 0x00}. Physical +// wiring only needs MOSI (on D0), SCK, CS — the other three data lines are +// left idle in this configuration. +#include "lgfx_panel_axs15231b.hpp" +class LGFX_JC3248W535 : public lgfx::LGFX_Device { + lgfx::Panel_AXS15231B _panel; + lgfx::Bus_SPI _bus; +public: + LGFX_JC3248W535() { + { + auto cfg = _bus.config(); + cfg.spi_host = SPI2_HOST; + cfg.spi_mode = 0; + cfg.freq_write = AXS_QSPI_FREQ; + cfg.freq_read = 16000000; + cfg.pin_sclk = AXS_QSPI_SCK; + cfg.pin_mosi = AXS_QSPI_D0; + cfg.pin_miso = -1; + cfg.pin_dc = -1; // no D/C line; command vs data is in the payload + cfg.use_lock = true; + _bus.config(cfg); + _panel.setBus(&_bus); + } + { + auto cfg = _panel.config(); + cfg.pin_cs = AXS_QSPI_CS; + cfg.pin_rst = -1; // no hardware reset pin — software reset used + cfg.pin_busy = -1; + cfg.memory_width = 320; + cfg.memory_height = 480; + cfg.panel_width = 320; + cfg.panel_height = 480; + cfg.offset_x = 0; + cfg.offset_y = 0; + cfg.offset_rotation = 0; + cfg.readable = false; + cfg.invert = false; + cfg.rgb_order = true; + cfg.dlen_16bit = false; + cfg.bus_shared = false; + _panel.config(cfg); + } + setPanel(&_panel); + } +}; +static LGFX_JC3248W535 _tft_instance; #elif defined(BOARD_IS_C3) // --- ESP32-C3 Super Mini + ST7789 240x280 ------------------------------------ diff --git a/src/lgfx_panel_axs15231b.hpp b/src/lgfx_panel_axs15231b.hpp new file mode 100644 index 0000000..98b993d --- /dev/null +++ b/src/lgfx_panel_axs15231b.hpp @@ -0,0 +1,421 @@ +// AXS15231B QSPI panel driver for LovyanGFX. +// +// Mainline LovyanGFX has no Panel_AXS15231B (issue #699). This class is +// modeled on lgfx::Panel_NV3041A — a 1.2.x panel with the same QSPI-header +// framing — and uses the AXS15231B init sequence from moononournation's +// Arduino_GFX (axs15231b_320480_type1_init_operations). +// +// Protocol: commands and pixel data flow over a standard single-wire SPI bus +// (lgfx::Bus_SPI). The QSPI part is pure framing: every command is prefixed +// with a 4-byte header {0x02, 0x00, cmd, 0x00} and RAMWR starts with +// {0x32, 0x00, 0x2C, 0x00}. Physical wiring only needs MOSI=D0, SCK, CS. + +#pragma once + +#if defined(ESP_PLATFORM) + +#include +#include +#include +#include +#include + +namespace lgfx { +inline namespace v1 { + +struct Panel_AXS15231B : public Panel_LCD { + protected: + static constexpr uint8_t CMD_NOP = 0x00; + static constexpr uint8_t CMD_SWRESET = 0x01; + static constexpr uint8_t CMD_SLPIN = 0x10; + static constexpr uint8_t CMD_SLPOUT = 0x11; + static constexpr uint8_t CMD_INVOFF = 0x20; + static constexpr uint8_t CMD_INVON = 0x21; + static constexpr uint8_t CMD_DISPOFF = 0x28; + static constexpr uint8_t CMD_DISPON = 0x29; + static constexpr uint8_t CMD_CASET = 0x2A; + static constexpr uint8_t CMD_RASET = 0x2B; + static constexpr uint8_t CMD_RAMWR = 0x2C; + static constexpr uint8_t CMD_MADCTL = 0x36; + static constexpr uint8_t CMD_COLMOD = 0x3A; + + static constexpr uint8_t CMD_MADCTL_MY = 0x80; + static constexpr uint8_t CMD_MADCTL_MX = 0x40; + static constexpr uint8_t CMD_MADCTL_MV = 0x20; + static constexpr uint8_t CMD_MADCTL_ML = 0x10; + static constexpr uint8_t CMD_MADCTL_RGB = 0x00; + + // Init sequence for 320x480 AXS15231B (type 1). Each entry is: + // { cmd_byte, arg_count, args[0..arg_count-1] } + // Sourced from Arduino_GFX axs15231b_320480_type1_init_operations[]. + // End-of-sequence marker: cmd_byte=0xFF, arg_count=0xFF. + static const uint8_t* init_ops() { + static const uint8_t ops[] = { + 0xA0, 17, + 0xC0, 0x10, 0x00, 0x02, 0x00, 0x00, 0x04, 0x3F, + 0x20, 0x05, 0x3F, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xA2, 31, + 0x30, 0x3C, 0x24, 0x14, 0xD0, 0x20, 0xFF, 0xE0, + 0x40, 0x19, 0x80, 0x80, 0x80, 0x20, 0xF9, 0x10, + 0x02, 0xFF, 0xFF, 0xF0, 0x90, 0x01, 0x32, 0xA0, + 0x91, 0xE0, 0x20, 0x7F, 0xFF, 0x00, 0x5A, + 0xD0, 30, + 0xE0, 0x40, 0x51, 0x24, 0x08, 0x05, 0x10, 0x01, + 0x20, 0x15, 0xC2, 0x42, 0x22, 0x22, 0xAA, 0x03, + 0x10, 0x12, 0x60, 0x14, 0x1E, 0x51, 0x15, 0x00, + 0x8A, 0x20, 0x00, 0x03, 0x3A, 0x12, + 0xA3, 22, + 0xA0, 0x06, 0xAA, 0x00, 0x08, 0x02, 0x0A, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x00, 0x55, 0x55, + 0xC1, 30, + 0x31, 0x04, 0x02, 0x02, 0x71, 0x05, 0x24, 0x55, + 0x02, 0x00, 0x41, 0x00, 0x53, 0xFF, 0xFF, 0xFF, + 0x4F, 0x52, 0x00, 0x4F, 0x52, 0x00, 0x45, 0x3B, + 0x0B, 0x02, 0x0D, 0x00, 0xFF, 0x40, + 0xC3, 11, + 0x00, 0x00, 0x00, 0x50, 0x03, 0x00, 0x00, 0x00, + 0x01, 0x80, 0x01, + 0xC4, 29, + 0x00, 0x24, 0x33, 0x80, 0x00, 0xEA, 0x64, 0x32, + 0xC8, 0x64, 0xC8, 0x32, 0x90, 0x90, 0x11, 0x06, + 0xDC, 0xFA, 0x00, 0x00, 0x80, 0xFE, 0x10, 0x10, + 0x00, 0x0A, 0x0A, 0x44, 0x50, + 0xC5, 23, + 0x18, 0x00, 0x00, 0x03, 0xFE, 0x3A, 0x4A, 0x20, + 0x30, 0x10, 0x88, 0xDE, 0x0D, 0x08, 0x0F, 0x0F, + 0x01, 0x3A, 0x4A, 0x20, 0x10, 0x10, 0x00, + 0xC6, 20, + 0x05, 0x0A, 0x05, 0x0A, 0x00, 0xE0, 0x2E, 0x0B, + 0x12, 0x22, 0x12, 0x22, 0x01, 0x03, 0x00, 0x3F, + 0x6A, 0x18, 0xC8, 0x22, + 0xC7, 20, + 0x50, 0x32, 0x28, 0x00, 0xA2, 0x80, 0x8F, 0x00, + 0x80, 0xFF, 0x07, 0x11, 0x9C, 0x67, 0xFF, 0x24, + 0x0C, 0x0D, 0x0E, 0x0F, + 0xC9, 4, + 0x33, 0x44, 0x44, 0x01, + 0xCF, 27, + 0x2C, 0x1E, 0x88, 0x58, 0x13, 0x18, 0x56, 0x18, + 0x1E, 0x68, 0x88, 0x00, 0x65, 0x09, 0x22, 0xC4, + 0x0C, 0x77, 0x22, 0x44, 0xAA, 0x55, 0x08, 0x08, + 0x12, 0xA0, 0x08, + 0xD5, 30, + 0x40, 0x8E, 0x8D, 0x01, 0x35, 0x04, 0x92, 0x74, + 0x04, 0x92, 0x74, 0x04, 0x08, 0x6A, 0x04, 0x46, + 0x03, 0x03, 0x03, 0x03, 0x82, 0x01, 0x03, 0x00, + 0xE0, 0x51, 0xA1, 0x00, 0x00, 0x00, + 0xD6, 30, + 0x10, 0x32, 0x54, 0x76, 0x98, 0xBA, 0xDC, 0xFE, + 0x93, 0x00, 0x01, 0x83, 0x07, 0x07, 0x00, 0x07, + 0x07, 0x00, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, + 0x00, 0x84, 0x00, 0x20, 0x01, 0x00, + 0xD7, 19, + 0x03, 0x01, 0x0B, 0x09, 0x0F, 0x0D, 0x1E, 0x1F, + 0x18, 0x1D, 0x1F, 0x19, 0x40, 0x8E, 0x04, 0x00, + 0x20, 0xA0, 0x1F, + 0xD8, 12, + 0x02, 0x00, 0x0A, 0x08, 0x0E, 0x0C, 0x1E, 0x1F, + 0x18, 0x1D, 0x1F, 0x19, + 0xD9, 12, + 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, + 0x1F, 0x1F, 0x1F, 0x1F, + 0xDD, 12, + 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, + 0x1F, 0x1F, 0x1F, 0x1F, + 0xDF, 8, + 0x44, 0x73, 0x4B, 0x69, 0x00, 0x0A, 0x02, 0x90, + 0xE0, 17, + 0x3B, 0x28, 0x10, 0x16, 0x0C, 0x06, 0x11, 0x28, + 0x5C, 0x21, 0x0D, 0x35, 0x13, 0x2C, 0x33, 0x28, 0x0D, + 0xE1, 17, + 0x37, 0x28, 0x10, 0x16, 0x0B, 0x06, 0x11, 0x28, + 0x5C, 0x21, 0x0D, 0x35, 0x14, 0x2C, 0x33, 0x28, 0x0F, + 0xE2, 17, + 0x3B, 0x07, 0x12, 0x18, 0x0E, 0x0D, 0x17, 0x35, + 0x44, 0x32, 0x0C, 0x14, 0x14, 0x36, 0x3A, 0x2F, 0x0D, + 0xE3, 17, + 0x37, 0x07, 0x12, 0x18, 0x0E, 0x0D, 0x17, 0x35, + 0x44, 0x32, 0x0C, 0x14, 0x14, 0x36, 0x32, 0x2F, 0x0F, + 0xE4, 17, + 0x3B, 0x07, 0x12, 0x18, 0x0E, 0x0D, 0x17, 0x39, + 0x44, 0x2E, 0x0C, 0x14, 0x14, 0x36, 0x3A, 0x2F, 0x0D, + 0xE5, 17, + 0x37, 0x07, 0x12, 0x18, 0x0E, 0x0D, 0x17, 0x39, + 0x44, 0x2E, 0x0C, 0x14, 0x14, 0x36, 0x3A, 0x2F, 0x0F, + 0xA4, 16, + 0x85, 0x85, 0x95, 0x82, 0xAF, 0xAA, 0xAA, 0x80, + 0x10, 0x30, 0x40, 0x40, 0x20, 0xFF, 0x60, 0x30, + 0xA4, 4, + 0x85, 0x85, 0x95, 0x85, + 0xBB, 8, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0xFF // end-of-sequence sentinel + }; + return ops; + } + + public: + Panel_AXS15231B(void) { + _cfg.memory_width = _cfg.panel_width = 320; + _cfg.memory_height = _cfg.panel_height = 480; + } + + bool init(bool use_reset) override { + if (!Panel_Device::init(use_reset)) return false; + + startWrite(); + + // Software reset (hardware RST not wired on JC3248W535). + cs_control(false); + write_cmd(CMD_SWRESET); + _bus->wait(); + cs_control(true); + delay(200); + + // Walk the init table. + const uint8_t* p = init_ops(); + while (p[0] != 0xFF || p[1] != 0xFF) { + uint8_t cmd = p[0]; + uint8_t n = p[1]; + cs_control(false); + write_cmd(cmd); + for (uint8_t i = 0; i < n; ++i) { + _bus->writeCommand(p[2 + i], 8); + } + _bus->wait(); + cs_control(true); + p += 2 + n; + } + + // Sleep out + display on. + cs_control(false); + write_cmd(CMD_SLPOUT); + _bus->wait(); + cs_control(true); + delay(200); + + cs_control(false); + write_cmd(CMD_DISPON); + _bus->wait(); + cs_control(true); + delay(100); + + endWrite(); + return true; + } + + void beginTransaction(void) override { + if (_in_transaction) return; + _in_transaction = true; + _bus->beginTransaction(); + } + + void endTransaction(void) override { + if (!_in_transaction) return; + _in_transaction = false; + if (_has_align_data) { + _has_align_data = false; + _bus->writeData(0, 8); + } + _bus->endTransaction(); + } + + color_depth_t setColorDepth(color_depth_t depth) override { + // AXS15231B supports RGB565 (0x05 in COLMOD) as the common choice for + // QSPI-wrapped transfers. RGB666 exists but is less common in the wild. + uint8_t mode; + if (depth == rgb565_2Byte) { + mode = 0x05; + } else if (depth == rgb666_3Byte) { + mode = 0x06; + } else { + return _write_depth; + } + _write_depth = depth; + startWrite(); + cs_control(false); + write_cmd(CMD_COLMOD); + _bus->writeCommand(mode, 8); + _bus->wait(); + cs_control(true); + endWrite(); + return _write_depth; + } + + void setInvert(bool invert) override { + cs_control(false); + write_cmd(invert ? CMD_INVON : CMD_INVOFF); + _bus->wait(); + cs_control(true); + } + + void setSleep(bool flg) override { + cs_control(false); + write_cmd(flg ? CMD_SLPIN : CMD_SLPOUT); + _bus->wait(); + cs_control(true); + if (!flg) delay(150); + } + + void setPowerSave(bool /*flg*/) override {} + void waitDisplay(void) override {} + bool displayBusy(void) override { return false; } + + void writePixels(pixelcopy_t* param, uint32_t len, bool use_dma) override { + start_qspi(); + if (param->no_convert) { + _bus->writeBytes(reinterpret_cast(param->src_data), + len * _write_bits >> 3, true, use_dma); + } else { + _bus->writePixels(param, len); + } + if (_cfg.dlen_16bit && (_write_bits & 15) && (len & 1)) { + _has_align_data = !_has_align_data; + } + _bus->wait(); + end_qspi(); + } + + void writeBlock(uint32_t rawcolor, uint32_t len) override { + start_qspi(); + _bus->writeDataRepeat(rawcolor, _write_bits, len); + _bus->wait(); + end_qspi(); + } + + void setWindow(uint_fast16_t xs, uint_fast16_t ys, + uint_fast16_t xe, uint_fast16_t ye) override { + if ((xe - xs) >= _width) { xs = 0; xe = _width - 1; } + if ((ye - ys) >= _height) { ys = 0; ye = _height - 1; } + + cs_control(false); + write_cmd(CMD_CASET); + _bus->writeCommand(xs >> 8, 8); + _bus->writeCommand(xs & 0xFF, 8); + _bus->writeCommand(xe >> 8, 8); + _bus->writeCommand(xe & 0xFF, 8); + _bus->wait(); + cs_control(true); + + cs_control(false); + write_cmd(CMD_RASET); + _bus->writeCommand(ys >> 8, 8); + _bus->writeCommand(ys & 0xFF, 8); + _bus->writeCommand(ye >> 8, 8); + _bus->writeCommand(ye & 0xFF, 8); + _bus->wait(); + cs_control(true); + + cs_control(false); + write_cmd(CMD_RAMWR); + _bus->wait(); + cs_control(true); + } + + void drawPixelPreclipped(uint_fast16_t x, uint_fast16_t y, + uint32_t rawcolor) override { + setWindow(x, y, x, y); + if (_cfg.dlen_16bit) { _has_align_data = (_write_bits & 15); } + start_qspi(); + _bus->writeData(rawcolor, _write_bits); + _bus->wait(); + end_qspi(); + } + + void writeFillRectPreclipped(uint_fast16_t x, uint_fast16_t y, + uint_fast16_t w, uint_fast16_t h, + uint32_t rawcolor) override { + uint32_t len = (uint32_t)w * h; + setWindow(x, y, x + w - 1, y + h - 1); + start_qspi(); + _bus->writeDataRepeat(rawcolor, _write_bits, len); + _bus->wait(); + end_qspi(); + } + + void writeImage(uint_fast16_t x, uint_fast16_t y, + uint_fast16_t w, uint_fast16_t h, + pixelcopy_t* param, bool use_dma) override { + setWindow(x, y, x + w - 1, y + h - 1); + start_qspi(); + auto sx = param->src_x; + auto bytes = param->dst_bits >> 3; + do { + if (param->no_convert) { + auto i = (param->src_bitwidth * param->src_y + sx) * bytes; + _bus->writeBytes(&((const uint8_t*)param->src_data)[i], + w * bytes, true, use_dma); + } else { + _bus->writePixels(param, w); + } + param->src_x = sx; + param->src_y++; + } while (--h); + _bus->wait(); + end_qspi(); + } + + // readRect is not supported (QSPI AMOLED-style panels are write-only). + uint32_t readCommand(uint_fast16_t /*cmd*/, uint_fast8_t /*index*/, + uint_fast8_t /*len*/) override { return 0; } + uint32_t readData(uint_fast8_t /*index*/, uint_fast8_t /*len*/) override { return 0; } + void readRect(uint_fast16_t /*x*/, uint_fast16_t /*y*/, + uint_fast16_t /*w*/, uint_fast16_t /*h*/, + void* /*dst*/, pixelcopy_t* /*param*/) override {} + + protected: + bool _in_transaction = false; + + void update_madctl(void) override { + uint8_t r = _internal_rotation; + uint8_t rgb = _cfg.rgb_order ? 0x08 : 0x00; // AXS15231B uses bit3 for BGR + uint8_t v; + switch (r) { + case 1: v = CMD_MADCTL_MX | CMD_MADCTL_MV | rgb; break; + case 2: v = CMD_MADCTL_MX | CMD_MADCTL_MY | rgb; break; + case 3: v = CMD_MADCTL_MY | CMD_MADCTL_MV | rgb; break; + default: v = rgb; break; + } + startWrite(); + cs_control(false); + write_cmd(CMD_MADCTL); + _bus->writeCommand(v, 8); + _bus->wait(); + cs_control(true); + endWrite(); + } + + // Emit a single-byte command prefixed by the QSPI header. + void write_cmd(uint8_t cmd) { + _bus->writeCommand(0x02, 8); + _bus->writeCommand(0x00, 8); + _bus->writeCommand(cmd, 8); + _bus->writeCommand(0x00, 8); + } + + // Enter pixel-data mode. After this, every byte written is pixel data + // until end_qspi() de-asserts CS. + void start_qspi() { + cs_control(false); + _bus->writeCommand(0x32, 8); + _bus->writeCommand(0x00, 8); + _bus->writeCommand(0x2C, 8); + _bus->writeCommand(0x00, 8); + _bus->wait(); + } + + void end_qspi() { + _bus->writeCommand(0x32, 8); + _bus->writeCommand(0x00, 8); + _bus->writeCommand(0x00, 8); + _bus->writeCommand(0x00, 8); + _bus->wait(); + cs_control(true); + } +}; + +} // namespace v1 +} // namespace lgfx + +#endif // ESP_PLATFORM From 51f271eb9b3b725eb4f743b93a488753b94fa0c9 Mon Sep 17 00:00:00 2001 From: Niels Timmer Date: Sun, 19 Apr 2026 14:19:12 +0200 Subject: [PATCH 03/15] Temporary JC3248W535 diagnostic: cycle R/G/B/W/K after init MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Run a 5-phase color-cycle (1.5 s each) immediately after display init on BOARD_IS_JC3248W535 to confirm whether pixel writes are actually landing in GRAM. Will be removed once the driver is solid. Observed on first flash: mostly solid light-blue panel with a small ~40x40 area at one corner showing varying lines across the cycle — i.e. only the first few KB of the pixel stream reach the panel. Rest of GRAM keeps its power-on state. Consistent with AXS15231B refusing single-wire pixel data after the RAMWR/QSPI header (the chip switches to 4-line input mode internally). Full QSPI via esp-idf spi_master is the next step. --- src/display_ui.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/display_ui.cpp b/src/display_ui.cpp index 8db844c..a447d61 100644 --- a/src/display_ui.cpp +++ b/src/display_ui.cpp @@ -436,6 +436,19 @@ void initDisplay() { tft.fillScreen(CLR_BG); Serial.println("Display: fillScreen done"); +#if defined(BOARD_IS_JC3248W535) + // TEMP diagnostic: cycle R/G/B so we can tell if pixel writes are landing. + // If the screen stays on one color through all three phases, the QSPI + // framing or color format is wrong. If it shows three distinct colors, + // pixels work and we're only chasing color-order / inversion. + Serial.println("Display: R"); tft.fillScreen(0xF800); delay(1500); + Serial.println("Display: G"); tft.fillScreen(0x07E0); delay(1500); + Serial.println("Display: B"); tft.fillScreen(0x001F); delay(1500); + Serial.println("Display: W"); tft.fillScreen(0xFFFF); delay(1500); + Serial.println("Display: K"); tft.fillScreen(0x0000); delay(1500); + Serial.println("Display: color cycle done"); +#endif + #if defined(TOUCH_CS) && !defined(USE_XPT2046) // LovyanGFX touch calibration uint16_t calData[8] = {0, 0, 0, 65535, 0, 65535, 65535, 65535}; From c49b762991a8ed90a2b2d3c1ec2a8f00c5aed56d Mon Sep 17 00:00:00 2001 From: Niels Timmer Date: Mon, 20 Apr 2026 16:18:44 +0200 Subject: [PATCH 04/15] JC3248W535 AXS15231B porting: diagnostics + vendor ESP-IDF driver shim Adds two standalone diagnostic build envs that coexist with the in-progress LovyanGFX production driver: - jc3248w535_skel: custom Bus_QSPI diagnostic in src/skeleton_test.cpp. Direct spi_device_polling_transmit calls, no LovyanGFX. SPI mode 3, 40 MHz, vendor init bytes. - jc3248w535_vendor: vendor ESP-IDF panel driver (src/vendor/esp_lcd_axs15231b.c, display-only fork) driven by a custom esp_lcd_panel_io_t shim (src/vendor/my_panel_io.c) that emits QSPI framing locally since arduino-esp32 3.0.17 lacks flags.quad_mode. Shim matches ESP-IDF v5.2 esp_lcd_panel_io_spi behavior: cmd as a separate QIO transaction, color data streamed in 4 KB DMA chunks, CS handling per chunk. Both envs run through init and drawing without errors but the panel still shows random noise. Hardware is confirmed working via the vendor prebuilt binary, so the bug is in our wire output. Next steps require either a logic analyzer or a rebuild against the vendor's exact arduino-esp32 toolchain. Also: - Gitignore 212 MB lib/arduino_esp32s3_libs_vendor/ staging dir (unused after the esp32s3-folder-swap attempt failed to compile against the vendor's newer FreeRTOS layout). - Minor WIP edits to production driver files (button.cpp, display_ui.cpp, lgfx_panel_axs15231b.hpp, main.cpp) carried over from earlier in the session; no functional change for other boards. --- .gitignore | 4 + platformio.ini | 50 ++- src/button.cpp | 30 +- src/display_ui.cpp | 93 +++-- src/lgfx_panel_axs15231b.hpp | 622 ++++++++++++++++++--------------- src/main.cpp | 8 + src/main_vendor.cpp | 187 ++++++++++ src/skeleton_test.cpp | 294 ++++++++++++++++ src/vendor/esp_lcd_axs15231b.c | 392 +++++++++++++++++++++ src/vendor/esp_lcd_axs15231b.h | 208 +++++++++++ src/vendor/esp_lcd_touch.h | 436 +++++++++++++++++++++++ src/vendor/my_panel_io.c | 160 +++++++++ src/vendor/my_panel_io.h | 25 ++ 13 files changed, 2165 insertions(+), 344 deletions(-) create mode 100644 src/main_vendor.cpp create mode 100644 src/skeleton_test.cpp create mode 100644 src/vendor/esp_lcd_axs15231b.c create mode 100644 src/vendor/esp_lcd_axs15231b.h create mode 100644 src/vendor/esp_lcd_touch.h create mode 100644 src/vendor/my_panel_io.c create mode 100644 src/vendor/my_panel_io.h diff --git a/.gitignore b/.gitignore index 3c06ffa..db7ffa0 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,7 @@ firmware/ *.map tools/pushall_dump.json tools/ams_dump.json + +# Vendor esp32s3 libs folder — 212 MB, copied locally during JC3248W535 +# driver work. Not needed for the build; kept outside the repo. +lib/arduino_esp32s3_libs_vendor/ diff --git a/platformio.ini b/platformio.ini index 11ff3f6..639f432 100644 --- a/platformio.ini +++ b/platformio.ini @@ -160,7 +160,7 @@ build_flags = ; --- Display (AXS15231B QSPI, 320x480 portrait native) --- -D DISPLAY_320x480=1 -D AXS_QSPI_HOST=1 ; SPI2_HOST - -D AXS_QSPI_FREQ=40000000 + -D AXS_QSPI_FREQ=32000000 ; Arduino_GFX caps AXS15231B at 32 MHz -D AXS_QSPI_CS=45 -D AXS_QSPI_SCK=47 -D AXS_QSPI_D0=21 @@ -172,6 +172,54 @@ build_flags = -D USE_AXS_TOUCH=1 -D AXS_TOUCH_SDA=4 -D AXS_TOUCH_SCL=8 + ; TEMP: bypass WiFi/MQTT/splash — only run display init + one rect then halt. + -D AXS_MINIMAL_TEST=1 + +; ============================================================================= +; jc3248w535_skel — standalone test binary using the axs15231b-lovyangfx +; skill's skeleton verbatim. No BambuHelper code. Used to isolate whether +; the issue is in BambuHelper's integration or at the driver/hw level. +; ============================================================================= +[env:jc3248w535_skel] +extends = env:jc3248w535 +build_src_filter = -<*> + +build_flags = + -D BOARD_VARIANT=\"jc3248w535_skel\" + -D BOARD_IS_JC3248W535_SKEL=1 + -D BOARD_HAS_PSRAM=1 + -D ARDUINO_USB_CDC_ON_BOOT=1 + +; ============================================================================= +; jc3248w535_vendor — baseline diagnostic. Compiles the MANUFACTURER'S +; ESP-IDF AXS15231B panel driver verbatim (src/vendor/esp_lcd_axs15231b.*), +; wired up via a minimal Arduino sketch that calls esp_lcd_new_panel_io_spi + +; esp_lcd_new_panel_axs15231b + esp_lcd_panel_draw_bitmap directly. If this +; binary paints RED/GREEN/BLUE/WHITE/BLACK then we have proof the toolchain +; works and the vendor driver is correct on our hardware — our custom +; LovyanGFX Bus_QSPI can then be deleted in favour of wrapping this. +; +; REQUIRES: the vendor-shipped esp32s3 framework libs have been swapped into +; ~/.platformio/packages/framework-arduinoespressif32/tools/sdk/esp32s3 so +; that esp_lcd_panel_io_spi_config_t::flags.quad_mode is defined. Done +; manually earlier in the development session. +; ============================================================================= +[env:jc3248w535_vendor] +; Compiles the vendor ESP-IDF AXS15231B panel driver verbatim, wired up via a +; CUSTOM esp_lcd_panel_io_t shim (src/vendor/my_panel_io.c) that implements the +; QSPI framing ourselves — so we don't depend on stock arduino-esp32 having the +; quad_mode flag in esp_lcd_panel_io_spi_config_t (it doesn't in 3.0.17). +; +; This isolates "vendor setWindow + RAMWR/RAMWRC sequencing" from "our wire +; framing": the vendor panel driver handles init-table walking, CASET, RAMWR +; dispatch; our shim handles the actual SPI transactions. If this paints, our +; wire framing is fine and the previous bug was in our setWindow/draw logic. +extends = env:jc3248w535 +build_src_filter = -<*> + + + +build_flags = + -D BOARD_VARIANT=\"jc3248w535_vendor\" + -D BOARD_IS_JC3248W535_VENDOR=1 + -D BOARD_HAS_PSRAM=1 + -D ARDUINO_USB_CDC_ON_BOOT=1 ; ============================================================================= ; ESP32-C3 Super Mini + ST7789 240x240 diff --git a/src/button.cpp b/src/button.cpp index 8e7bfe3..0514cc5 100644 --- a/src/button.cpp +++ b/src/button.cpp @@ -29,13 +29,14 @@ } #elif defined(USE_AXS_TOUCH) // AXS15231B integrated touch controller. I2C slave at 0x3B. - // Reading a touch point requires writing an 11-byte command packet then - // reading 14 bytes back; status byte [1] & 0x0F holds touch count. - // Protocol documented in me-processware/JC3248W535-Driver. + // Protocol (per axs15231b-lovyangfx skill): write 8-byte command, read 8 + // bytes back. Touch is active when rx[0] == 0 (no gesture) AND rx[1] != 0 + // (finger count > 0). Coordinates arrive pre-scaled to panel resolution, + // NOT raw 0-4095. Single-touch only. #include #define AXS_TOUCH_ADDR 0x3B - static const uint8_t AXS_READ_TOUCHPAD_CMD[11] = { - 0xb5, 0xab, 0xa5, 0x5a, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00 + static const uint8_t AXS_READ_TOUCHPAD_CMD[8] = { + 0xB5, 0xAB, 0xA5, 0x5A, 0x00, 0x00, 0x00, 0x08 }; static bool axsTouchBusReady = false; static bool axsTouchSeen = false; @@ -45,20 +46,21 @@ return Wire.endTransmission(true) == 0; } - // Reads current touch state. Returns true if a touch is active and fills x/y - // with raw panel coordinates (native 320x480, caller applies any rotation). + // Returns true if a touch is active and fills x/y with panel-scaled + // coordinates (native portrait 320x480 — caller applies rotation). static bool axsTouchRead(uint16_t& x, uint16_t& y) { Wire.beginTransmission(AXS_TOUCH_ADDR); Wire.write(AXS_READ_TOUCHPAD_CMD, sizeof(AXS_READ_TOUCHPAD_CMD)); if (Wire.endTransmission() != 0) return false; - uint8_t buf[14] = {0}; - size_t got = Wire.requestFrom((uint8_t)AXS_TOUCH_ADDR, (uint8_t)14); + uint8_t rx[8] = {0}; + size_t got = Wire.requestFrom((uint8_t)AXS_TOUCH_ADDR, (uint8_t)8); if (got < 6) return false; - for (size_t i = 0; i < got && i < sizeof(buf); ++i) buf[i] = Wire.read(); - uint8_t touches = buf[1] & 0x0F; - if (touches == 0) return false; - x = ((uint16_t)(buf[2] & 0x0F) << 8) | buf[3]; - y = ((uint16_t)(buf[4] & 0x0F) << 8) | buf[5]; + for (size_t i = 0; i < got && i < sizeof(rx); ++i) rx[i] = Wire.read(); + // Valid plain-touch: gesture=0 and at least one finger down. + if (rx[0] != 0) return false; + if ((rx[1] & 0x0F) == 0) return false; + x = ((uint16_t)(rx[2] & 0x0F) << 8) | rx[3]; + y = ((uint16_t)(rx[4] & 0x0F) << 8) | rx[5]; return true; } #elif defined(TOUCH_CS) diff --git a/src/display_ui.cpp b/src/display_ui.cpp index a447d61..3084637 100644 --- a/src/display_ui.cpp +++ b/src/display_ui.cpp @@ -205,51 +205,38 @@ static LGFX_WS154 _tft_instance; #elif defined(BOARD_IS_JC3248W535) // --- Guition JC3248W535 + AXS15231B 320x480 --------------------------------- -// Panel_AXS15231B is implemented locally (see lgfx_panel_axs15231b.hpp) since -// mainline LovyanGFX does not ship it. The controller uses QSPI framing on -// top of a regular single-wire SPI bus: every command is wrapped in a 4-byte -// header, and pixel data starts with {0x32, 0x00, 0x2C, 0x00}. Physical -// wiring only needs MOSI (on D0), SCK, CS — the other three data lines are -// left idle in this configuration. +// Panel_AXS15231B is implemented locally (see lgfx_panel_axs15231b.hpp) and +// owns its own ESP-IDF spi_master bus in QSPI (4-line data) mode — mainline +// LovyanGFX has neither the driver nor a QSPI bus class. LGFX_Device's _bus +// pointer stays null: all pixel and command traffic goes through the panel's +// own transaction calls. CS is still driven by Panel_Device::cs_control() +// via cfg.pin_cs. #include "lgfx_panel_axs15231b.hpp" class LGFX_JC3248W535 : public lgfx::LGFX_Device { lgfx::Panel_AXS15231B _panel; - lgfx::Bus_SPI _bus; public: LGFX_JC3248W535() { - { - auto cfg = _bus.config(); - cfg.spi_host = SPI2_HOST; - cfg.spi_mode = 0; - cfg.freq_write = AXS_QSPI_FREQ; - cfg.freq_read = 16000000; - cfg.pin_sclk = AXS_QSPI_SCK; - cfg.pin_mosi = AXS_QSPI_D0; - cfg.pin_miso = -1; - cfg.pin_dc = -1; // no D/C line; command vs data is in the payload - cfg.use_lock = true; - _bus.config(cfg); - _panel.setBus(&_bus); - } - { - auto cfg = _panel.config(); - cfg.pin_cs = AXS_QSPI_CS; - cfg.pin_rst = -1; // no hardware reset pin — software reset used - cfg.pin_busy = -1; - cfg.memory_width = 320; - cfg.memory_height = 480; - cfg.panel_width = 320; - cfg.panel_height = 480; - cfg.offset_x = 0; - cfg.offset_y = 0; - cfg.offset_rotation = 0; - cfg.readable = false; - cfg.invert = false; - cfg.rgb_order = true; - cfg.dlen_16bit = false; - cfg.bus_shared = false; - _panel.config(cfg); - } + _panel.setBusPins((spi_host_device_t)AXS_QSPI_HOST, + AXS_QSPI_SCK, + AXS_QSPI_D0, AXS_QSPI_D1, AXS_QSPI_D2, AXS_QSPI_D3, + AXS_QSPI_FREQ); + auto cfg = _panel.config(); + cfg.pin_cs = AXS_QSPI_CS; + cfg.pin_rst = -1; // no hardware reset wired; software reset in init + cfg.pin_busy = -1; + cfg.memory_width = 320; + cfg.memory_height = 480; + cfg.panel_width = 320; + cfg.panel_height = 480; + cfg.offset_x = 0; + cfg.offset_y = 0; + cfg.offset_rotation = 0; + cfg.readable = false; + cfg.invert = false; + cfg.rgb_order = false; // AXS15231B wants MADCTL bit3=1 for RGB + cfg.dlen_16bit = false; + cfg.bus_shared = false; + _panel.config(cfg); setPanel(&_panel); } }; @@ -436,18 +423,6 @@ void initDisplay() { tft.fillScreen(CLR_BG); Serial.println("Display: fillScreen done"); -#if defined(BOARD_IS_JC3248W535) - // TEMP diagnostic: cycle R/G/B so we can tell if pixel writes are landing. - // If the screen stays on one color through all three phases, the QSPI - // framing or color format is wrong. If it shows three distinct colors, - // pixels work and we're only chasing color-order / inversion. - Serial.println("Display: R"); tft.fillScreen(0xF800); delay(1500); - Serial.println("Display: G"); tft.fillScreen(0x07E0); delay(1500); - Serial.println("Display: B"); tft.fillScreen(0x001F); delay(1500); - Serial.println("Display: W"); tft.fillScreen(0xFFFF); delay(1500); - Serial.println("Display: K"); tft.fillScreen(0x0000); delay(1500); - Serial.println("Display: color cycle done"); -#endif #if defined(TOUCH_CS) && !defined(USE_XPT2046) // LovyanGFX touch calibration @@ -464,6 +439,16 @@ void initDisplay() { memset(&prevState, 0, sizeof(prevState)); // Splash screen +#if defined(BOARD_IS_JC3248W535) + // Minimal-minimal test pattern. ONE 8-aligned red square away from the + // origin. If setWindow works, we see RED at (96, 200) size 32x24. If + // broken, we see red at (0,0) with extent 32x24 (or some variant). + Serial.println("Display: minimal test — RED 32x24 at (96, 200)"); + tft.fillRect(96, 200, 32, 24, 0xF800); +# if defined(AXS_MINIMAL_TEST) + return; // no splash, no drawString — main.cpp halts after we return +# endif +#endif tft.setTextDatum(MC_DATUM); tft.setTextColor(CLR_GREEN, CLR_BG); tft.setTextFont(4); @@ -473,6 +458,10 @@ void initDisplay() { tft.drawString("Printer Monitor", SCREEN_W / 2, SCREEN_H / 2 + 10); tft.setTextFont(1); tft.drawString(FW_VERSION, SCREEN_W / 2, SCREEN_H / 2 + 30); +#if defined(BOARD_IS_JC3248W535) + Serial.println("Display: splash hold"); + delay(8000); +#endif } void applyDisplaySettings() { diff --git a/src/lgfx_panel_axs15231b.hpp b/src/lgfx_panel_axs15231b.hpp index 98b993d..791d419 100644 --- a/src/lgfx_panel_axs15231b.hpp +++ b/src/lgfx_panel_axs15231b.hpp @@ -1,14 +1,19 @@ -// AXS15231B QSPI panel driver for LovyanGFX. +// AXS15231B QSPI panel driver for LovyanGFX (ESP32-S3). // -// Mainline LovyanGFX has no Panel_AXS15231B (issue #699). This class is -// modeled on lgfx::Panel_NV3041A — a 1.2.x panel with the same QSPI-header -// framing — and uses the AXS15231B init sequence from moononournation's -// Arduino_GFX (axs15231b_320480_type1_init_operations). +// Mainline LovyanGFX has no Panel_AXS15231B (issue #699). The chip requires +// real 4-line QSPI for pixel data — single-wire MOSI works for init but the +// controller switches to quad-mode input after the RAMWR/RAMWRCONT header +// and stops accepting data on D0 alone. Arduino_GFX's Arduino_ESP32QSPI +// uses ESP-IDF's spi_master with SPI_TRANS_MODE_QIO on the data path; we do +// the same here. This class inherits Panel_LCD so LovyanGFX's graphics +// layer (fillRect, drawString, smoothArc, sprites, VLW fonts) is unchanged. // -// Protocol: commands and pixel data flow over a standard single-wire SPI bus -// (lgfx::Bus_SPI). The QSPI part is pure framing: every command is prefixed -// with a 4-byte header {0x02, 0x00, cmd, 0x00} and RAMWR starts with -// {0x32, 0x00, 0x2C, 0x00}. Physical wiring only needs MOSI=D0, SCK, CS. +// Protocol: +// Commands: cmd=0x02, addr={0x00,cmd,0x00} (24 bits), single-line on D0. +// Optional byte-data is sent single-line on D0 after the header. +// Pixels: cmd=0x32, addr={0x00,0x3C,0x00} (24 bits), QIO on D0..D3. +// (0x3C = RAMWRCONT, appended after the 0x2C RAMWR that +// setWindow() issues.) #pragma once @@ -16,16 +21,25 @@ #include #include -#include #include #include +#include +#include +#include +#include +#include +#include +#include +#include + namespace lgfx { inline namespace v1 { struct Panel_AXS15231B : public Panel_LCD { - protected: - static constexpr uint8_t CMD_NOP = 0x00; + static constexpr uint32_t AXS_CHUNK_PIXELS = 1024; // matches Arduino_ESP32QSPI + static constexpr uint32_t AXS_CHUNK_BYTES = AXS_CHUNK_PIXELS * 2; + static constexpr uint8_t CMD_SWRESET = 0x01; static constexpr uint8_t CMD_SLPIN = 0x10; static constexpr uint8_t CMD_SLPOUT = 0x11; @@ -39,222 +53,171 @@ struct Panel_AXS15231B : public Panel_LCD { static constexpr uint8_t CMD_MADCTL = 0x36; static constexpr uint8_t CMD_COLMOD = 0x3A; - static constexpr uint8_t CMD_MADCTL_MY = 0x80; - static constexpr uint8_t CMD_MADCTL_MX = 0x40; - static constexpr uint8_t CMD_MADCTL_MV = 0x20; - static constexpr uint8_t CMD_MADCTL_ML = 0x10; - static constexpr uint8_t CMD_MADCTL_RGB = 0x00; - - // Init sequence for 320x480 AXS15231B (type 1). Each entry is: - // { cmd_byte, arg_count, args[0..arg_count-1] } - // Sourced from Arduino_GFX axs15231b_320480_type1_init_operations[]. - // End-of-sequence marker: cmd_byte=0xFF, arg_count=0xFF. + // Init table — 320x480 type1, ported from moononournation/Arduino_GFX. + // Layout: cmd, arg_count, args... (sentinel 0xFF 0xFF ends the list). static const uint8_t* init_ops() { static const uint8_t ops[] = { 0xA0, 17, - 0xC0, 0x10, 0x00, 0x02, 0x00, 0x00, 0x04, 0x3F, - 0x20, 0x05, 0x3F, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xC0, 0x10, 0x00, 0x02, 0x00, 0x00, 0x04, 0x3F, 0x20, 0x05, + 0x3F, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, 0xA2, 31, - 0x30, 0x3C, 0x24, 0x14, 0xD0, 0x20, 0xFF, 0xE0, - 0x40, 0x19, 0x80, 0x80, 0x80, 0x20, 0xF9, 0x10, - 0x02, 0xFF, 0xFF, 0xF0, 0x90, 0x01, 0x32, 0xA0, - 0x91, 0xE0, 0x20, 0x7F, 0xFF, 0x00, 0x5A, + 0x30, 0x3C, 0x24, 0x14, 0xD0, 0x20, 0xFF, 0xE0, 0x40, 0x19, + 0x80, 0x80, 0x80, 0x20, 0xF9, 0x10, 0x02, 0xFF, 0xFF, 0xF0, + 0x90, 0x01, 0x32, 0xA0, 0x91, 0xE0, 0x20, 0x7F, 0xFF, 0x00, 0x5A, 0xD0, 30, - 0xE0, 0x40, 0x51, 0x24, 0x08, 0x05, 0x10, 0x01, - 0x20, 0x15, 0xC2, 0x42, 0x22, 0x22, 0xAA, 0x03, - 0x10, 0x12, 0x60, 0x14, 0x1E, 0x51, 0x15, 0x00, - 0x8A, 0x20, 0x00, 0x03, 0x3A, 0x12, + 0xE0, 0x40, 0x51, 0x24, 0x08, 0x05, 0x10, 0x01, 0x20, 0x15, + 0xC2, 0x42, 0x22, 0x22, 0xAA, 0x03, 0x10, 0x12, 0x60, 0x14, + 0x1E, 0x51, 0x15, 0x00, 0x8A, 0x20, 0x00, 0x03, 0x3A, 0x12, 0xA3, 22, - 0xA0, 0x06, 0xAA, 0x00, 0x08, 0x02, 0x0A, 0x04, - 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, - 0x04, 0x04, 0x04, 0x00, 0x55, 0x55, + 0xA0, 0x06, 0xAA, 0x00, 0x08, 0x02, 0x0A, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x00, 0x55, 0x55, 0xC1, 30, - 0x31, 0x04, 0x02, 0x02, 0x71, 0x05, 0x24, 0x55, - 0x02, 0x00, 0x41, 0x00, 0x53, 0xFF, 0xFF, 0xFF, - 0x4F, 0x52, 0x00, 0x4F, 0x52, 0x00, 0x45, 0x3B, - 0x0B, 0x02, 0x0D, 0x00, 0xFF, 0x40, + 0x31, 0x04, 0x02, 0x02, 0x71, 0x05, 0x24, 0x55, 0x02, 0x00, + 0x41, 0x00, 0x53, 0xFF, 0xFF, 0xFF, 0x4F, 0x52, 0x00, 0x4F, + 0x52, 0x00, 0x45, 0x3B, 0x0B, 0x02, 0x0D, 0x00, 0xFF, 0x40, 0xC3, 11, - 0x00, 0x00, 0x00, 0x50, 0x03, 0x00, 0x00, 0x00, - 0x01, 0x80, 0x01, + 0x00, 0x00, 0x00, 0x50, 0x03, 0x00, 0x00, 0x00, 0x01, 0x80, 0x01, 0xC4, 29, - 0x00, 0x24, 0x33, 0x80, 0x00, 0xEA, 0x64, 0x32, - 0xC8, 0x64, 0xC8, 0x32, 0x90, 0x90, 0x11, 0x06, - 0xDC, 0xFA, 0x00, 0x00, 0x80, 0xFE, 0x10, 0x10, - 0x00, 0x0A, 0x0A, 0x44, 0x50, + 0x00, 0x24, 0x33, 0x80, 0x00, 0xEA, 0x64, 0x32, 0xC8, 0x64, + 0xC8, 0x32, 0x90, 0x90, 0x11, 0x06, 0xDC, 0xFA, 0x00, 0x00, + 0x80, 0xFE, 0x10, 0x10, 0x00, 0x0A, 0x0A, 0x44, 0x50, 0xC5, 23, - 0x18, 0x00, 0x00, 0x03, 0xFE, 0x3A, 0x4A, 0x20, - 0x30, 0x10, 0x88, 0xDE, 0x0D, 0x08, 0x0F, 0x0F, - 0x01, 0x3A, 0x4A, 0x20, 0x10, 0x10, 0x00, + 0x18, 0x00, 0x00, 0x03, 0xFE, 0x3A, 0x4A, 0x20, 0x30, 0x10, + 0x88, 0xDE, 0x0D, 0x08, 0x0F, 0x0F, 0x01, 0x3A, 0x4A, 0x20, + 0x10, 0x10, 0x00, 0xC6, 20, - 0x05, 0x0A, 0x05, 0x0A, 0x00, 0xE0, 0x2E, 0x0B, - 0x12, 0x22, 0x12, 0x22, 0x01, 0x03, 0x00, 0x3F, - 0x6A, 0x18, 0xC8, 0x22, + 0x05, 0x0A, 0x05, 0x0A, 0x00, 0xE0, 0x2E, 0x0B, 0x12, 0x22, + 0x12, 0x22, 0x01, 0x03, 0x00, 0x3F, 0x6A, 0x18, 0xC8, 0x22, 0xC7, 20, - 0x50, 0x32, 0x28, 0x00, 0xA2, 0x80, 0x8F, 0x00, - 0x80, 0xFF, 0x07, 0x11, 0x9C, 0x67, 0xFF, 0x24, - 0x0C, 0x0D, 0x0E, 0x0F, - 0xC9, 4, - 0x33, 0x44, 0x44, 0x01, + 0x50, 0x32, 0x28, 0x00, 0xA2, 0x80, 0x8F, 0x00, 0x80, 0xFF, + 0x07, 0x11, 0x9C, 0x67, 0xFF, 0x24, 0x0C, 0x0D, 0x0E, 0x0F, + 0xC9, 4, 0x33, 0x44, 0x44, 0x01, 0xCF, 27, - 0x2C, 0x1E, 0x88, 0x58, 0x13, 0x18, 0x56, 0x18, - 0x1E, 0x68, 0x88, 0x00, 0x65, 0x09, 0x22, 0xC4, - 0x0C, 0x77, 0x22, 0x44, 0xAA, 0x55, 0x08, 0x08, - 0x12, 0xA0, 0x08, + 0x2C, 0x1E, 0x88, 0x58, 0x13, 0x18, 0x56, 0x18, 0x1E, 0x68, + 0x88, 0x00, 0x65, 0x09, 0x22, 0xC4, 0x0C, 0x77, 0x22, 0x44, + 0xAA, 0x55, 0x08, 0x08, 0x12, 0xA0, 0x08, 0xD5, 30, - 0x40, 0x8E, 0x8D, 0x01, 0x35, 0x04, 0x92, 0x74, - 0x04, 0x92, 0x74, 0x04, 0x08, 0x6A, 0x04, 0x46, - 0x03, 0x03, 0x03, 0x03, 0x82, 0x01, 0x03, 0x00, - 0xE0, 0x51, 0xA1, 0x00, 0x00, 0x00, + 0x40, 0x8E, 0x8D, 0x01, 0x35, 0x04, 0x92, 0x74, 0x04, 0x92, + 0x74, 0x04, 0x08, 0x6A, 0x04, 0x46, 0x03, 0x03, 0x03, 0x03, + 0x82, 0x01, 0x03, 0x00, 0xE0, 0x51, 0xA1, 0x00, 0x00, 0x00, 0xD6, 30, - 0x10, 0x32, 0x54, 0x76, 0x98, 0xBA, 0xDC, 0xFE, - 0x93, 0x00, 0x01, 0x83, 0x07, 0x07, 0x00, 0x07, - 0x07, 0x00, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, - 0x00, 0x84, 0x00, 0x20, 0x01, 0x00, + 0x10, 0x32, 0x54, 0x76, 0x98, 0xBA, 0xDC, 0xFE, 0x93, 0x00, + 0x01, 0x83, 0x07, 0x07, 0x00, 0x07, 0x07, 0x00, 0x03, 0x03, + 0x03, 0x03, 0x03, 0x03, 0x00, 0x84, 0x00, 0x20, 0x01, 0x00, 0xD7, 19, - 0x03, 0x01, 0x0B, 0x09, 0x0F, 0x0D, 0x1E, 0x1F, - 0x18, 0x1D, 0x1F, 0x19, 0x40, 0x8E, 0x04, 0x00, - 0x20, 0xA0, 0x1F, + 0x03, 0x01, 0x0B, 0x09, 0x0F, 0x0D, 0x1E, 0x1F, 0x18, 0x1D, + 0x1F, 0x19, 0x40, 0x8E, 0x04, 0x00, 0x20, 0xA0, 0x1F, 0xD8, 12, - 0x02, 0x00, 0x0A, 0x08, 0x0E, 0x0C, 0x1E, 0x1F, - 0x18, 0x1D, 0x1F, 0x19, + 0x02, 0x00, 0x0A, 0x08, 0x0E, 0x0C, 0x1E, 0x1F, 0x18, 0x1D, 0x1F, 0x19, 0xD9, 12, - 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, - 0x1F, 0x1F, 0x1F, 0x1F, + 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0xDD, 12, - 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, - 0x1F, 0x1F, 0x1F, 0x1F, - 0xDF, 8, - 0x44, 0x73, 0x4B, 0x69, 0x00, 0x0A, 0x02, 0x90, + 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, + 0xDF, 8, 0x44, 0x73, 0x4B, 0x69, 0x00, 0x0A, 0x02, 0x90, 0xE0, 17, - 0x3B, 0x28, 0x10, 0x16, 0x0C, 0x06, 0x11, 0x28, - 0x5C, 0x21, 0x0D, 0x35, 0x13, 0x2C, 0x33, 0x28, 0x0D, + 0x3B, 0x28, 0x10, 0x16, 0x0C, 0x06, 0x11, 0x28, 0x5C, 0x21, + 0x0D, 0x35, 0x13, 0x2C, 0x33, 0x28, 0x0D, 0xE1, 17, - 0x37, 0x28, 0x10, 0x16, 0x0B, 0x06, 0x11, 0x28, - 0x5C, 0x21, 0x0D, 0x35, 0x14, 0x2C, 0x33, 0x28, 0x0F, + 0x37, 0x28, 0x10, 0x16, 0x0B, 0x06, 0x11, 0x28, 0x5C, 0x21, + 0x0D, 0x35, 0x14, 0x2C, 0x33, 0x28, 0x0F, 0xE2, 17, - 0x3B, 0x07, 0x12, 0x18, 0x0E, 0x0D, 0x17, 0x35, - 0x44, 0x32, 0x0C, 0x14, 0x14, 0x36, 0x3A, 0x2F, 0x0D, + 0x3B, 0x07, 0x12, 0x18, 0x0E, 0x0D, 0x17, 0x35, 0x44, 0x32, + 0x0C, 0x14, 0x14, 0x36, 0x3A, 0x2F, 0x0D, 0xE3, 17, - 0x37, 0x07, 0x12, 0x18, 0x0E, 0x0D, 0x17, 0x35, - 0x44, 0x32, 0x0C, 0x14, 0x14, 0x36, 0x32, 0x2F, 0x0F, + 0x37, 0x07, 0x12, 0x18, 0x0E, 0x0D, 0x17, 0x35, 0x44, 0x32, + 0x0C, 0x14, 0x14, 0x36, 0x32, 0x2F, 0x0F, 0xE4, 17, - 0x3B, 0x07, 0x12, 0x18, 0x0E, 0x0D, 0x17, 0x39, - 0x44, 0x2E, 0x0C, 0x14, 0x14, 0x36, 0x3A, 0x2F, 0x0D, + 0x3B, 0x07, 0x12, 0x18, 0x0E, 0x0D, 0x17, 0x39, 0x44, 0x2E, + 0x0C, 0x14, 0x14, 0x36, 0x3A, 0x2F, 0x0D, 0xE5, 17, - 0x37, 0x07, 0x12, 0x18, 0x0E, 0x0D, 0x17, 0x39, - 0x44, 0x2E, 0x0C, 0x14, 0x14, 0x36, 0x3A, 0x2F, 0x0F, + 0x37, 0x07, 0x12, 0x18, 0x0E, 0x0D, 0x17, 0x39, 0x44, 0x2E, + 0x0C, 0x14, 0x14, 0x36, 0x3A, 0x2F, 0x0F, 0xA4, 16, - 0x85, 0x85, 0x95, 0x82, 0xAF, 0xAA, 0xAA, 0x80, - 0x10, 0x30, 0x40, 0x40, 0x20, 0xFF, 0x60, 0x30, - 0xA4, 4, - 0x85, 0x85, 0x95, 0x85, - 0xBB, 8, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xFF, 0xFF // end-of-sequence sentinel + 0x85, 0x85, 0x95, 0x82, 0xAF, 0xAA, 0xAA, 0x80, 0x10, 0x30, + 0x40, 0x40, 0x20, 0xFF, 0x60, 0x30, + 0xA4, 4, 0x85, 0x85, 0x95, 0x85, + 0xBB, 8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0xFF // end sentinel }; return ops; } - public: Panel_AXS15231B(void) { _cfg.memory_width = _cfg.panel_width = 320; _cfg.memory_height = _cfg.panel_height = 480; } - bool init(bool use_reset) override { - if (!Panel_Device::init(use_reset)) return false; - - startWrite(); + ~Panel_AXS15231B() { + if (_spi_dev) { spi_bus_remove_device(_spi_dev); _spi_dev = nullptr; } + if (_bus_attached) { spi_bus_free(_spi_host); _bus_attached = false; } + if (_dma_buf) { heap_caps_free(_dma_buf); _dma_buf = nullptr; } + } - // Software reset (hardware RST not wired on JC3248W535). - cs_control(false); - write_cmd(CMD_SWRESET); - _bus->wait(); - cs_control(true); - delay(200); - - // Walk the init table. - const uint8_t* p = init_ops(); - while (p[0] != 0xFF || p[1] != 0xFF) { - uint8_t cmd = p[0]; - uint8_t n = p[1]; - cs_control(false); - write_cmd(cmd); - for (uint8_t i = 0; i < n; ++i) { - _bus->writeCommand(p[2 + i], 8); - } - _bus->wait(); - cs_control(true); - p += 2 + n; - } + // Pin / clock setup — called before init(). The panel owns its own SPI + // bus; Panel_LCD's _bus pointer is ignored. + void setTrace(bool on) { _trace = on; } + + void setBusPins(spi_host_device_t host, int8_t sck, + int8_t d0, int8_t d1, int8_t d2, int8_t d3, + uint32_t freq_hz) { + _spi_host = host; + _pin_sck = sck; + _pin_d0 = d0; _pin_d1 = d1; _pin_d2 = d2; _pin_d3 = d3; + _freq = freq_hz; + } - // Sleep out + display on. - cs_control(false); - write_cmd(CMD_SLPOUT); - _bus->wait(); - cs_control(true); - delay(200); + bool init(bool use_reset) override { + if (!Panel_Device::init(use_reset)) return false; + if (!ensure_bus()) return false; - cs_control(false); - write_cmd(CMD_DISPON); - _bus->wait(); - cs_control(true); delay(100); - endWrite(); + // Absolute minimal init — match the skill's skeleton verbatim: + // SWRESET, DISPOFF, SLPIN, SLPOUT, DISPON, COLMOD=0x05. + // No MADCTL (use factory default orientation for this test). + send_cmd(CMD_SWRESET); delay(200); + send_cmd(CMD_DISPOFF); delay(20); + send_cmd(CMD_SLPIN); delay(20); + send_cmd(CMD_SLPOUT); delay(200); + send_cmd(CMD_DISPON); delay(20); + + uint8_t colmod = 0x05; + send_cmd(CMD_COLMOD, &colmod, 1); + delay(10); + _write_depth = rgb565_2Byte; + _write_bits = 16; + return true; } void beginTransaction(void) override { - if (_in_transaction) return; - _in_transaction = true; - _bus->beginTransaction(); + if (_in_tx) return; + _in_tx = true; + if (_spi_dev) spi_device_acquire_bus(_spi_dev, portMAX_DELAY); } void endTransaction(void) override { - if (!_in_transaction) return; - _in_transaction = false; - if (_has_align_data) { - _has_align_data = false; - _bus->writeData(0, 8); - } - _bus->endTransaction(); + if (!_in_tx) return; + _in_tx = false; + flush_pixels(); // safety net — close any open pixel batch + if (_spi_dev) spi_device_release_bus(_spi_dev); } color_depth_t setColorDepth(color_depth_t depth) override { - // AXS15231B supports RGB565 (0x05 in COLMOD) as the common choice for - // QSPI-wrapped transfers. RGB666 exists but is less common in the wild. - uint8_t mode; - if (depth == rgb565_2Byte) { - mode = 0x05; - } else if (depth == rgb666_3Byte) { - mode = 0x06; - } else { - return _write_depth; - } - _write_depth = depth; - startWrite(); - cs_control(false); - write_cmd(CMD_COLMOD); - _bus->writeCommand(mode, 8); - _bus->wait(); - cs_control(true); - endWrite(); + uint8_t mode = 0; + if (depth == rgb565_2Byte) { mode = 0x05; _write_depth = depth; } + else if (depth == rgb666_3Byte) { mode = 0x06; _write_depth = depth; } + else return _write_depth; + send_cmd(CMD_COLMOD, &mode, 1); return _write_depth; } void setInvert(bool invert) override { - cs_control(false); - write_cmd(invert ? CMD_INVON : CMD_INVOFF); - _bus->wait(); - cs_control(true); + send_cmd(invert ? CMD_INVON : CMD_INVOFF); } void setSleep(bool flg) override { - cs_control(false); - write_cmd(flg ? CMD_SLPIN : CMD_SLPOUT); - _bus->wait(); - cs_control(true); + send_cmd(flg ? CMD_SLPIN : CMD_SLPOUT); if (!flg) delay(150); } @@ -262,65 +225,31 @@ struct Panel_AXS15231B : public Panel_LCD { void waitDisplay(void) override {} bool displayBusy(void) override { return false; } - void writePixels(pixelcopy_t* param, uint32_t len, bool use_dma) override { - start_qspi(); - if (param->no_convert) { - _bus->writeBytes(reinterpret_cast(param->src_data), - len * _write_bits >> 3, true, use_dma); - } else { - _bus->writePixels(param, len); - } - if (_cfg.dlen_16bit && (_write_bits & 15) && (len & 1)) { - _has_align_data = !_has_align_data; - } - _bus->wait(); - end_qspi(); - } - - void writeBlock(uint32_t rawcolor, uint32_t len) override { - start_qspi(); - _bus->writeDataRepeat(rawcolor, _write_bits, len); - _bus->wait(); - end_qspi(); - } - void setWindow(uint_fast16_t xs, uint_fast16_t ys, uint_fast16_t xe, uint_fast16_t ye) override { if ((xe - xs) >= _width) { xs = 0; xe = _width - 1; } if ((ye - ys) >= _height) { ys = 0; ye = _height - 1; } - - cs_control(false); - write_cmd(CMD_CASET); - _bus->writeCommand(xs >> 8, 8); - _bus->writeCommand(xs & 0xFF, 8); - _bus->writeCommand(xe >> 8, 8); - _bus->writeCommand(xe & 0xFF, 8); - _bus->wait(); - cs_control(true); - - cs_control(false); - write_cmd(CMD_RASET); - _bus->writeCommand(ys >> 8, 8); - _bus->writeCommand(ys & 0xFF, 8); - _bus->writeCommand(ye >> 8, 8); - _bus->writeCommand(ye & 0xFF, 8); - _bus->wait(); - cs_control(true); - - cs_control(false); - write_cmd(CMD_RAMWR); - _bus->wait(); - cs_control(true); + // Recovery gap after a QIO pixel batch — the panel sometimes refuses + // to accept a single-line CASET/RASET immediately after quad data. + esp_rom_delay_us(2); + uint8_t buf[4]; + buf[0] = xs >> 8; buf[1] = xs & 0xFF; buf[2] = xe >> 8; buf[3] = xe & 0xFF; + send_cmd(CMD_CASET, buf, 4); + esp_rom_delay_us(2); + buf[0] = ys >> 8; buf[1] = ys & 0xFF; buf[2] = ye >> 8; buf[3] = ye & 0xFF; + send_cmd(CMD_RASET, buf, 4); + esp_rom_delay_us(2); + _pixel_first = true; + Serial.printf("[AXS] setWindow (%u,%u)-(%u,%u)\n", + (unsigned)xs, (unsigned)ys, (unsigned)xe, (unsigned)ye); } void drawPixelPreclipped(uint_fast16_t x, uint_fast16_t y, uint32_t rawcolor) override { setWindow(x, y, x, y); - if (_cfg.dlen_16bit) { _has_align_data = (_write_bits & 15); } - start_qspi(); - _bus->writeData(rawcolor, _write_bits); - _bus->wait(); - end_qspi(); + // Single-pixel path — route through send_pixels_repeat_qio so the DMA + // buffer is used (stack pointers aren't DMA-capable on ESP32-S3). + send_pixels_repeat_qio((uint16_t)rawcolor, 1); } void writeFillRectPreclipped(uint_fast16_t x, uint_fast16_t y, @@ -328,90 +257,229 @@ struct Panel_AXS15231B : public Panel_LCD { uint32_t rawcolor) override { uint32_t len = (uint32_t)w * h; setWindow(x, y, x + w - 1, y + h - 1); - start_qspi(); - _bus->writeDataRepeat(rawcolor, _write_bits, len); - _bus->wait(); - end_qspi(); + send_pixels_repeat_qio((uint16_t)rawcolor, len); + } + + void writeBlock(uint32_t rawcolor, uint32_t len) override { + send_pixels_repeat_qio((uint16_t)rawcolor, len); + } + + void writePixels(pixelcopy_t* param, uint32_t len, bool /*use_dma*/) override { + // Convert into the DMA staging buffer in AXS_CHUNK_PIXELS-sized chunks + // and byte-swap for the panel (AXS15231B wants big-endian RGB565). + // send_pixels_qio_chunk consults _pixel_first so the first chunk after + // setWindow opens with RAMWR and everything after uses RAMWRCONT, even + // across multiple writePixels calls (e.g. writeImage looping rows). + uint8_t* staging = dma_buf(); + if (!staging) return; + while (len) { + uint32_t chunk = (len > AXS_CHUNK_PIXELS) ? AXS_CHUNK_PIXELS : len; + param->fp_copy(staging, 0, chunk, param); + uint16_t* dst = reinterpret_cast(staging); + for (uint32_t i = 0; i < chunk; ++i) { + uint16_t p = dst[i]; + dst[i] = (uint16_t)((p << 8) | (p >> 8)); + } + send_pixels_qio_chunk(staging, chunk * 2); + len -= chunk; + } + // writeImage calls writePixels per row — DON'T flush here or each row + // starts a fresh batch. writeImage handles flush at its end. } void writeImage(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h, pixelcopy_t* param, bool use_dma) override { + if (_trace) Serial.printf("[AXS] writeImage xy=(%u,%u) wh=(%u,%u)\n", + (unsigned)x, (unsigned)y, (unsigned)w, (unsigned)h); setWindow(x, y, x + w - 1, y + h - 1); - start_qspi(); auto sx = param->src_x; - auto bytes = param->dst_bits >> 3; do { - if (param->no_convert) { - auto i = (param->src_bitwidth * param->src_y + sx) * bytes; - _bus->writeBytes(&((const uint8_t*)param->src_data)[i], - w * bytes, true, use_dma); - } else { - _bus->writePixels(param, w); - } + writePixels(param, w, use_dma); param->src_x = sx; param->src_y++; } while (--h); - _bus->wait(); - end_qspi(); + flush_pixels(); } - // readRect is not supported (QSPI AMOLED-style panels are write-only). - uint32_t readCommand(uint_fast16_t /*cmd*/, uint_fast8_t /*index*/, - uint_fast8_t /*len*/) override { return 0; } - uint32_t readData(uint_fast8_t /*index*/, uint_fast8_t /*len*/) override { return 0; } - void readRect(uint_fast16_t /*x*/, uint_fast16_t /*y*/, - uint_fast16_t /*w*/, uint_fast16_t /*h*/, - void* /*dst*/, pixelcopy_t* /*param*/) override {} + // Readback is not supported (write-only QSPI panel). + uint32_t readCommand(uint_fast16_t, uint_fast8_t, uint_fast8_t) override { return 0; } + uint32_t readData(uint_fast8_t, uint_fast8_t) override { return 0; } + void readRect(uint_fast16_t, uint_fast16_t, uint_fast16_t, uint_fast16_t, + void*, pixelcopy_t*) override {} protected: - bool _in_transaction = false; + spi_host_device_t _spi_host = SPI2_HOST; + spi_device_handle_t _spi_dev = nullptr; + bool _bus_attached = false; + bool _in_tx = false; + // True until the first pixel chunk after the most recent setWindow has + // been pushed. That chunk sends RAMWR (0x002C00) to anchor at the top + // of the window; every subsequent chunk sends RAMWRCONT (0x003C00) so + // the panel appends instead of re-anchoring. + bool _pixel_first = true; + // Debug — toggle at runtime to trace window/image/pixel calls. Cheap + // when off, noisy when on. Set by Panel_AXS15231B::setTrace(). + bool _trace = false; + int8_t _pin_sck = -1, _pin_d0 = -1, _pin_d1 = -1, _pin_d2 = -1, _pin_d3 = -1; + uint32_t _freq = 40000000; + uint8_t* _dma_buf = nullptr; + + uint8_t* dma_buf() { + if (!_dma_buf) { + _dma_buf = (uint8_t*)heap_caps_aligned_alloc( + 16, AXS_CHUNK_BYTES, MALLOC_CAP_DMA); + } + return _dma_buf; + } + + // Bypass Panel_Device::cs_control — go directly to GPIO to match the + // skeleton and Arduino_ESP32QSPI exactly. Panel_Device's default might + // do extra bookkeeping we don't need and that may interact badly with + // our manual pin management. + void cs_control(bool level) override { + if (_cfg.pin_cs >= 0) { + gpio_set_level((gpio_num_t)_cfg.pin_cs, level ? 1 : 0); + } + } + + bool ensure_bus() { + if (_spi_dev) return true; + if (_cfg.pin_cs >= 0) { + gpio_set_direction((gpio_num_t)_cfg.pin_cs, GPIO_MODE_OUTPUT); + gpio_set_level((gpio_num_t)_cfg.pin_cs, 1); + } + spi_bus_config_t bus = {}; + bus.mosi_io_num = _pin_d0; + bus.miso_io_num = _pin_d1; // ESP-IDF uses MISO pin slot for D1 + bus.sclk_io_num = _pin_sck; + bus.quadwp_io_num = _pin_d2; + bus.quadhd_io_num = _pin_d3; + bus.max_transfer_sz = AXS_CHUNK_BYTES + 16; + bus.flags = SPICOMMON_BUSFLAG_MASTER | SPICOMMON_BUSFLAG_GPIO_PINS; + esp_err_t err = spi_bus_initialize(_spi_host, &bus, SPI_DMA_CH_AUTO); + if (err != ESP_OK) { + ESP_LOGE("AXS15231B", "spi_bus_initialize failed: %d", err); + return false; + } + _bus_attached = true; + + spi_device_interface_config_t dev = {}; + dev.command_bits = 8; + dev.address_bits = 24; + dev.mode = 0; + dev.clock_speed_hz = _freq; + dev.spics_io_num = -1; // CS is handled by Panel_Device::cs_control + dev.queue_size = 1; + dev.flags = SPI_DEVICE_HALFDUPLEX; + err = spi_bus_add_device(_spi_host, &dev, &_spi_dev); + if (err != ESP_OK) { + ESP_LOGE("AXS15231B", "spi_bus_add_device failed: %d", err); + return false; + } + return dma_buf() != nullptr; + } void update_madctl(void) override { - uint8_t r = _internal_rotation; - uint8_t rgb = _cfg.rgb_order ? 0x08 : 0x00; // AXS15231B uses bit3 for BGR + uint8_t r = _internal_rotation; + // Match Panel_NV3041A's rgb_order convention: rgb_order=true -> RGB (0x00), + // rgb_order=false -> BGR (0x08). AXS15231B's MADCTL bit3 = BGR. + uint8_t rgb = _cfg.rgb_order ? 0x00 : 0x08; uint8_t v; switch (r) { - case 1: v = CMD_MADCTL_MX | CMD_MADCTL_MV | rgb; break; - case 2: v = CMD_MADCTL_MX | CMD_MADCTL_MY | rgb; break; - case 3: v = CMD_MADCTL_MY | CMD_MADCTL_MV | rgb; break; + case 1: v = 0x40 | 0x20 | rgb; break; // MX|MV + case 2: v = 0x40 | 0x80 | rgb; break; // MX|MY + case 3: v = 0x80 | 0x20 | rgb; break; // MY|MV default: v = rgb; break; } - startWrite(); - cs_control(false); - write_cmd(CMD_MADCTL); - _bus->writeCommand(v, 8); - _bus->wait(); - cs_control(true); - endWrite(); + send_cmd(CMD_MADCTL, &v, 1); + if (_trace) Serial.printf("[AXS] MADCTL = 0x%02X (rot=%u rgb_order=%d)\n", + v, (unsigned)r, (int)_cfg.rgb_order); } - // Emit a single-byte command prefixed by the QSPI header. - void write_cmd(uint8_t cmd) { - _bus->writeCommand(0x02, 8); - _bus->writeCommand(0x00, 8); - _bus->writeCommand(cmd, 8); - _bus->writeCommand(0x00, 8); + // Single-line command, optionally followed by single-line data bytes. + // Matches Arduino_ESP32QSPI exactly — small payloads (<=4 bytes) go via + // SPI_TRANS_USE_TXDATA (inline in the transaction struct, internal SPI + // FIFO — no DMA), which is how writeC8D16D16 handles CASET/RASET. Larger + // payloads use the DMA staging buffer. + void send_cmd(uint8_t cmd, const uint8_t* data = nullptr, size_t len = 0) { + if (!_spi_dev) return; + flush_pixels(); + cs_control(false); + spi_transaction_ext_t t = {}; + t.base.flags = SPI_TRANS_MULTILINE_CMD | SPI_TRANS_MULTILINE_ADDR; + t.base.cmd = 0x02; + t.base.addr = ((uint32_t)cmd) << 8; + if (len == 0) { + t.base.tx_buffer = nullptr; + t.base.length = 0; + } else if (len <= 4) { + t.base.flags |= SPI_TRANS_USE_TXDATA; + for (size_t i = 0; i < len; ++i) t.base.tx_data[i] = data[i]; + t.base.length = len * 8; + } else { + uint8_t* staging = dma_buf(); + if (!staging) { cs_control(true); return; } + size_t copy = (len > AXS_CHUNK_BYTES) ? AXS_CHUNK_BYTES : len; + memcpy(staging, data, copy); + t.base.tx_buffer = staging; + t.base.length = copy * 8; + } + spi_device_polling_transmit(_spi_dev, &t.base); + cs_control(true); } - // Enter pixel-data mode. After this, every byte written is pixel data - // until end_qspi() de-asserts CS. - void start_qspi() { + // Send one chunk of pixel data in QIO mode. Matches the skill skeleton / + // LilyGo T-Display-S3 Long pattern: CS toggles per chunk, full header + // sent every time. First chunk uses RAMWR (0x002C00) to anchor at the + // setWindow origin; continuations use RAMWRCONT (0x003C00) to append. + // A single microsecond CS-high gap between chunks prevents the panel + // from interpreting the next header as appended pixel data. + void send_pixels_qio_chunk(const void* buf, size_t bytes) { + if (!_spi_dev || bytes == 0) return; + bool first = _pixel_first; + _pixel_first = false; cs_control(false); - _bus->writeCommand(0x32, 8); - _bus->writeCommand(0x00, 8); - _bus->writeCommand(0x2C, 8); - _bus->writeCommand(0x00, 8); - _bus->wait(); + spi_transaction_ext_t t = {}; + t.base.flags = SPI_TRANS_MODE_QIO; + t.base.cmd = 0x32; + t.base.addr = first ? 0x002C00 : 0x003C00; + t.base.tx_buffer = buf; + t.base.length = bytes * 8; + esp_err_t err = spi_device_polling_transmit(_spi_dev, &t.base); + cs_control(true); + esp_rom_delay_us(1); + if (err != ESP_OK) { + ESP_LOGE("AXS15231B", "chunk bytes=%u err=%d", (unsigned)bytes, (int)err); + } } - void end_qspi() { - _bus->writeCommand(0x32, 8); - _bus->writeCommand(0x00, 8); - _bus->writeCommand(0x00, 8); - _bus->writeCommand(0x00, 8); - _bus->wait(); - cs_control(true); + // No-op now that CS toggles per chunk — kept so send_cmd / endTransaction + // callers don't need to know about it. Also resets _pixel_first so the + // next setWindow starts a fresh batch with a RAMWR header. + void flush_pixels() { _pixel_first = true; } + + + // Fill the DMA staging buffer with `color` (RGB565, panel byte order) and + // clock it out in chunks. `count` is pixel count. + void send_pixels_repeat_qio(uint16_t color, uint32_t count) { + uint8_t* staging = dma_buf(); + if (!staging) return; + uint16_t be = (uint16_t)((color << 8) | (color >> 8)); + uint32_t fill_pixels = (count < AXS_CHUNK_PIXELS) ? count : AXS_CHUNK_PIXELS; + uint16_t* dst = reinterpret_cast(staging); + for (uint32_t i = 0; i < fill_pixels; ++i) dst[i] = be; + + const uint32_t total = count; + uint32_t sent = 0; + while (count) { + uint32_t chunk = (count > AXS_CHUNK_PIXELS) ? AXS_CHUNK_PIXELS : count; + send_pixels_qio_chunk(staging, chunk * 2); + count -= chunk; + sent += chunk; + } + flush_pixels(); + (void)sent; (void)total; } }; diff --git a/src/main.cpp b/src/main.cpp index 6cc125f..0598f15 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -449,6 +449,14 @@ void setup() { loadSettings(); initDisplay(); +#if defined(BOARD_IS_JC3248W535) && defined(AXS_MINIMAL_TEST) + // MINIMAL-MINIMAL test: nothing after display init. No WiFi, no MQTT, + // no web server, no splash overlay. setBacklight() so we can actually + // see the panel, then halt. Flash over USB (COM12) to iterate. + Serial.println("AXS_MINIMAL_TEST: halting"); + setBacklight(128); + while (true) { delay(1000); } +#endif splashEnd = millis() + 2000; startWiFiDuringSplash(); setBacklight(brightness); diff --git a/src/main_vendor.cpp b/src/main_vendor.cpp new file mode 100644 index 0000000..8dd1f00 --- /dev/null +++ b/src/main_vendor.cpp @@ -0,0 +1,187 @@ +// Vendor-baseline + custom-shim diagnostic for the JC3248W535 AXS15231B. +// +// Uses the MANUFACTURER'S ESP-IDF panel driver (src/vendor/esp_lcd_axs15231b.c) +// via a CUSTOM esp_lcd_panel_io_t shim (src/vendor/my_panel_io.c) that handles +// the QSPI wire framing. This side-steps the missing `quad_mode` flag in stock +// arduino-esp32 3.0.17 while keeping all vendor-sequencing logic intact. +// +// Guarded so that only the jc3248w535_vendor env compiles this TU. + +#ifdef BOARD_IS_JC3248W535_VENDOR + +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +#include "vendor/esp_lcd_axs15231b.h" +#include "vendor/my_panel_io.h" +} + +#define PIN_QSPI_CS 45 +#define PIN_QSPI_SCK 47 +#define PIN_QSPI_D0 21 +#define PIN_QSPI_D1 48 +#define PIN_QSPI_D2 40 +#define PIN_QSPI_D3 39 +#define PIN_BL 1 +#define PIN_SDA 4 +#define PIN_SCL 8 + +#define LCD_W 320 +#define LCD_H 480 + +static spi_device_handle_t g_spi = nullptr; +static esp_lcd_panel_io_handle_t g_io = nullptr; +static esp_lcd_panel_handle_t g_panel = nullptr; + +// Vendor BSP override init table (from esp_bsp.c lines 34-67). +static const axs15231b_lcd_init_cmd_t kInitCmds[] = { + {0xBB, (uint8_t[]){0x00,0x00,0x00,0x00,0x00,0x00,0x5A,0xA5}, 8, 0}, + {0xA0, (uint8_t[]){0xC0,0x10,0x00,0x02,0x00,0x00,0x04,0x3F,0x20,0x05,0x3F,0x3F,0x00,0x00,0x00,0x00,0x00}, 17, 0}, + {0xA2, (uint8_t[]){0x30,0x3C,0x24,0x14,0xD0,0x20,0xFF,0xE0,0x40,0x19,0x80,0x80,0x80,0x20,0xF9,0x10,0x02,0xFF,0xFF,0xF0,0x90,0x01,0x32,0xA0,0x91,0xE0,0x20,0x7F,0xFF,0x00,0x5A}, 31, 0}, + {0xD0, (uint8_t[]){0xE0,0x40,0x51,0x24,0x08,0x05,0x10,0x01,0x20,0x15,0x42,0xC2,0x22,0x22,0xAA,0x03,0x10,0x12,0x60,0x14,0x1E,0x51,0x15,0x00,0x8A,0x20,0x00,0x03,0x3A,0x12}, 30, 0}, + {0xA3, (uint8_t[]){0xA0,0x06,0xAA,0x00,0x08,0x02,0x0A,0x04,0x04,0x04,0x04,0x04,0x04,0x04,0x04,0x04,0x04,0x04,0x04,0x00,0x55,0x55}, 22, 0}, + {0xC1, (uint8_t[]){0x31,0x04,0x02,0x02,0x71,0x05,0x24,0x55,0x02,0x00,0x41,0x00,0x53,0xFF,0xFF,0xFF,0x4F,0x52,0x00,0x4F,0x52,0x00,0x45,0x3B,0x0B,0x02,0x0D,0x00,0xFF,0x40}, 30, 0}, + {0xC3, (uint8_t[]){0x00,0x00,0x00,0x50,0x03,0x00,0x00,0x00,0x01,0x80,0x01}, 11, 0}, + {0xC4, (uint8_t[]){0x00,0x24,0x33,0x80,0x00,0xEA,0x64,0x32,0xC8,0x64,0xC8,0x32,0x90,0x90,0x11,0x06,0xDC,0xFA,0x00,0x00,0x80,0xFE,0x10,0x10,0x00,0x0A,0x0A,0x44,0x50}, 29, 0}, + {0xC5, (uint8_t[]){0x18,0x00,0x00,0x03,0xFE,0x3A,0x4A,0x20,0x30,0x10,0x88,0xDE,0x0D,0x08,0x0F,0x0F,0x01,0x3A,0x4A,0x20,0x10,0x10,0x00}, 23, 0}, + {0xC6, (uint8_t[]){0x05,0x0A,0x05,0x0A,0x00,0xE0,0x2E,0x0B,0x12,0x22,0x12,0x22,0x01,0x03,0x00,0x3F,0x6A,0x18,0xC8,0x22}, 20, 0}, + {0xC7, (uint8_t[]){0x50,0x32,0x28,0x00,0xA2,0x80,0x8F,0x00,0x80,0xFF,0x07,0x11,0x9C,0x67,0xFF,0x24,0x0C,0x0D,0x0E,0x0F}, 20, 0}, + {0xC9, (uint8_t[]){0x33,0x44,0x44,0x01}, 4, 0}, + {0xCF, (uint8_t[]){0x2C,0x1E,0x88,0x58,0x13,0x18,0x56,0x18,0x1E,0x68,0x88,0x00,0x65,0x09,0x22,0xC4,0x0C,0x77,0x22,0x44,0xAA,0x55,0x08,0x08,0x12,0xA0,0x08}, 27, 0}, + {0xD5, (uint8_t[]){0x40,0x8E,0x8D,0x01,0x35,0x04,0x92,0x74,0x04,0x92,0x74,0x04,0x08,0x6A,0x04,0x46,0x03,0x03,0x03,0x03,0x82,0x01,0x03,0x00,0xE0,0x51,0xA1,0x00,0x00,0x00}, 30, 0}, + {0xD6, (uint8_t[]){0x10,0x32,0x54,0x76,0x98,0xBA,0xDC,0xFE,0x93,0x00,0x01,0x83,0x07,0x07,0x00,0x07,0x07,0x00,0x03,0x03,0x03,0x03,0x03,0x03,0x00,0x84,0x00,0x20,0x01,0x00}, 30, 0}, + {0xD7, (uint8_t[]){0x03,0x01,0x0B,0x09,0x0F,0x0D,0x1E,0x1F,0x18,0x1D,0x1F,0x19,0x40,0x8E,0x04,0x00,0x20,0xA0,0x1F}, 19, 0}, + {0xD8, (uint8_t[]){0x02,0x00,0x0A,0x08,0x0E,0x0C,0x1E,0x1F,0x18,0x1D,0x1F,0x19}, 12, 0}, + {0xD9, (uint8_t[]){0x1F,0x1F,0x1F,0x1F,0x1F,0x1F,0x1F,0x1F,0x1F,0x1F,0x1F,0x1F}, 12, 0}, + {0xDD, (uint8_t[]){0x1F,0x1F,0x1F,0x1F,0x1F,0x1F,0x1F,0x1F,0x1F,0x1F,0x1F,0x1F}, 12, 0}, + {0xDF, (uint8_t[]){0x44,0x73,0x4B,0x69,0x00,0x0A,0x02,0x90}, 8, 0}, + {0xE0, (uint8_t[]){0x3B,0x28,0x10,0x16,0x0C,0x06,0x11,0x28,0x5C,0x21,0x0D,0x35,0x13,0x2C,0x33,0x28,0x0D}, 17, 0}, + {0xE1, (uint8_t[]){0x37,0x28,0x10,0x16,0x0B,0x06,0x11,0x28,0x5C,0x21,0x0D,0x35,0x14,0x2C,0x33,0x28,0x0F}, 17, 0}, + {0xE2, (uint8_t[]){0x3B,0x07,0x12,0x18,0x0E,0x0D,0x17,0x35,0x44,0x32,0x0C,0x14,0x14,0x36,0x3A,0x2F,0x0D}, 17, 0}, + {0xE3, (uint8_t[]){0x37,0x07,0x12,0x18,0x0E,0x0D,0x17,0x35,0x44,0x32,0x0C,0x14,0x14,0x36,0x32,0x2F,0x0F}, 17, 0}, + {0xE4, (uint8_t[]){0x3B,0x07,0x12,0x18,0x0E,0x0D,0x17,0x39,0x44,0x2E,0x0C,0x14,0x14,0x36,0x3A,0x2F,0x0D}, 17, 0}, + {0xE5, (uint8_t[]){0x37,0x07,0x12,0x18,0x0E,0x0D,0x17,0x39,0x44,0x2E,0x0C,0x14,0x14,0x36,0x3A,0x2F,0x0F}, 17, 0}, + {0xA4, (uint8_t[]){0x85,0x85,0x95,0x82,0xAF,0xAA,0xAA,0x80,0x10,0x30,0x40,0x40,0x20,0xFF,0x60,0x30}, 16, 0}, + {0xA4, (uint8_t[]){0x85,0x85,0x95,0x85}, 4, 0}, + {0xBB, (uint8_t[]){0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, 8, 0}, + {0x13, (uint8_t[]){0x00}, 0, 0}, + {0x11, (uint8_t[]){0x00}, 0, 120}, + {0x2C, (uint8_t[]){0x00,0x00,0x00,0x00}, 4, 0}, +}; + +static void backlight_init(uint8_t pct) { + ledc_timer_config_t t = {}; + t.speed_mode = LEDC_LOW_SPEED_MODE; t.duty_resolution = LEDC_TIMER_10_BIT; + t.timer_num = LEDC_TIMER_1; t.freq_hz = 5000; t.clk_cfg = LEDC_AUTO_CLK; + ledc_timer_config(&t); + ledc_channel_config_t c = {}; + c.gpio_num = PIN_BL; c.speed_mode = LEDC_LOW_SPEED_MODE; + c.channel = LEDC_CHANNEL_1; c.timer_sel = LEDC_TIMER_1; + c.duty = (1023u * pct) / 100u; c.hpoint = 0; + ledc_channel_config(&c); +} + +static void paint(uint16_t c565) { + const size_t N = (size_t)LCD_W * LCD_H; + uint16_t* buf = (uint16_t*)ps_malloc(N * sizeof(uint16_t)); + if (!buf) { Serial.println("alloc fail"); return; } + uint16_t sw = __builtin_bswap16(c565); + for (size_t i = 0; i < N; ++i) buf[i] = sw; + esp_lcd_panel_draw_bitmap(g_panel, 0, 0, LCD_W, LCD_H, buf); + free(buf); +} + +void setup() { + Serial.begin(115200); + delay(300); + Serial.println("\n=== VENDOR DRIVER + CUSTOM SHIM ==="); + + backlight_init(100); + Wire.begin(PIN_SDA, PIN_SCL); + Wire.setClock(400000); + + // SPI bus. CS is driven by the peripheral (not GPIO) so our shim can use + // SPI_TRANS_CS_KEEP_ACTIVE to hold CS across the 32-bit cmd + data chunks, + // matching stock esp_lcd_panel_io_spi behavior. + spi_bus_config_t bcfg = {}; + bcfg.mosi_io_num = PIN_QSPI_D0; + bcfg.miso_io_num = PIN_QSPI_D1; + bcfg.sclk_io_num = PIN_QSPI_SCK; + bcfg.quadwp_io_num = PIN_QSPI_D2; + bcfg.quadhd_io_num = PIN_QSPI_D3; + bcfg.max_transfer_sz = 4096; + bcfg.flags = SPICOMMON_BUSFLAG_MASTER | SPICOMMON_BUSFLAG_GPIO_PINS + | SPICOMMON_BUSFLAG_QUAD; // force D2/D3 to be driven as outputs + esp_err_t r = spi_bus_initialize(SPI2_HOST, &bcfg, SPI_DMA_CH_AUTO); + Serial.printf("[bsp] spi_bus_initialize -> %d\n", r); + + spi_device_interface_config_t dcfg = {}; + dcfg.command_bits = 0; + dcfg.address_bits = 0; + dcfg.mode = 3; + dcfg.clock_speed_hz = 40 * 1000 * 1000; + dcfg.spics_io_num = PIN_QSPI_CS; // peripheral-driven CS for KEEP_ACTIVE + dcfg.queue_size = 10; // stock esp_lcd uses 10 + dcfg.flags = SPI_DEVICE_HALFDUPLEX; + r = spi_bus_add_device(SPI2_HOST, &dcfg, &g_spi); + Serial.printf("[bsp] spi_bus_add_device -> %d\n", r); + + // Create our custom panel_io, then hand it to the vendor panel driver. + r = my_panel_io_new(g_spi, PIN_QSPI_CS, &g_io); + Serial.printf("[bsp] my_panel_io_new -> %d\n", r); + + axs15231b_vendor_config_t vcfg = {}; + // Let the panel driver use its built-in `vendor_specific_init_default` + // table (passing NULL init_cmds). That's the "type 1" 320x480 init which + // ends with DISPON + ALLPOFF(0x22). The esp_bsp.c override table we used + // before ends without ALLPOFF which may leave the chip in an indeterminate + // DDRAM-display state producing our noise symptom. + vcfg.init_cmds = NULL; + vcfg.init_cmds_size = 0; + vcfg.flags.use_qspi_interface = 1; + esp_lcd_panel_dev_config_t pcfg = {}; + pcfg.reset_gpio_num = -1; + pcfg.color_space = LCD_RGB_ELEMENT_ORDER_RGB; + pcfg.bits_per_pixel = 16; + pcfg.vendor_config = &vcfg; + r = esp_lcd_new_panel_axs15231b(g_io, &pcfg, &g_panel); + Serial.printf("[bsp] new_panel_axs15231b -> %d\n", r); + + esp_lcd_panel_reset(g_panel); + esp_lcd_panel_init(g_panel); + esp_lcd_panel_disp_on_off(g_panel, true); + Serial.println("[bsp] panel on"); + + const struct { uint16_t c; const char* n; } seq[] = { + {0xF800,"RED"},{0x07E0,"GREEN"},{0x001F,"BLUE"}, + {0xFFFF,"WHITE"},{0x0000,"BLACK"}, + }; + for (auto& s : seq) { Serial.printf("paint %s\n", s.n); paint(s.c); delay(800); } + Serial.println("halted"); +} + +static bool touch_down() { + static const uint8_t cmd[11] = { 0xB5,0xAB,0xA5,0x5A, 0x00,0x00, 0x00,0x08, 0x00,0x00,0x00 }; + Wire.beginTransmission(0x3B); + Wire.write(cmd, sizeof(cmd)); + if (Wire.endTransmission() != 0) return false; + uint8_t b[8] = {0}; + size_t got = Wire.requestFrom((uint8_t)0x3B, (uint8_t)8); + if (got < 6) return false; + for (size_t i = 0; i < got && i < sizeof(b); ++i) b[i] = Wire.read(); + return (b[0] == 0) && ((b[1] & 0x0F) != 0); +} + +void loop() { + if (touch_down()) { Serial.println("touch → restart"); delay(50); ESP.restart(); } + delay(30); +} + +#endif // BOARD_IS_JC3248W535_VENDOR diff --git a/src/skeleton_test.cpp b/src/skeleton_test.cpp new file mode 100644 index 0000000..e380514 --- /dev/null +++ b/src/skeleton_test.cpp @@ -0,0 +1,294 @@ +// Bare-metal AXS15231B diagnostic — no LovyanGFX, no Panel_LCD, no abstraction. +// +// Goal: answer one question — does our QSPI framing produce correct pixel data? +// +// The init sequence has been debugged to match the vendor byte-for-byte. If the +// init bytes + SPI mode 3 + pclk 40MHz + COLMOD 0x55 + RGB565 are all correct +// AND the panel is armed (DISPON sent), then a single writePixels call with a +// solid-color buffer should paint the screen that color. If it doesn't, the +// bug lives in the pixel-write code path (how we frame 0x32/0x002C00 + QIO data +// + CS management). If it does paint, the LovyanGFX integration was the bug. +// +// Touch-to-reset is kept so iteration loop remains fast. + +#include +#include +#include +#include +#include +#include + +// --- Pins (JC3248W535, verified from vendor pincfg.h) --- +#define PIN_CS GPIO_NUM_45 +#define PIN_SCK GPIO_NUM_47 +#define PIN_D0 GPIO_NUM_21 +#define PIN_D1 GPIO_NUM_48 +#define PIN_D2 GPIO_NUM_40 +#define PIN_D3 GPIO_NUM_39 +#define PIN_BL 1 +#define PIN_SDA 4 +#define PIN_SCL 8 + +#define LCD_W 320 +#define LCD_H 480 +#define CHUNK_PIXELS 1024 + +static spi_device_handle_t g_spi = nullptr; +static uint8_t* g_dma = nullptr; // reused for pixel chunks + +// Send a command via the AXS "write-cmd" path (opcode 0x02, single-line). +// 4-byte header on the wire: 0x02, 0x00, cmd, 0x00, then optional data bytes. +static void cs_low() { gpio_set_level(PIN_CS, 0); } +static void cs_high() { gpio_set_level(PIN_CS, 1); } + +static void tx_cmd(uint8_t cmd, const uint8_t* data, size_t n) { + cs_low(); + spi_transaction_ext_t t = {}; + // Vendor BSP sends init commands over QIO (lcd_cmd_bits=32 + quad_mode=true). + // Adding MODE_QIO here makes our init header go 4-line like the vendor's — + // previous attempts without it went single-line and the chip's behaviour on + // receiving an init byte sequence timed at SIO vs QIO may be the bug. + t.base.flags = SPI_TRANS_MODE_QIO + | SPI_TRANS_MULTILINE_CMD + | SPI_TRANS_MULTILINE_ADDR; + t.base.cmd = 0x02; + t.base.addr = ((uint32_t)cmd) << 8; + if (n) { + t.base.tx_buffer = data; + t.base.length = n * 8; + } + spi_device_polling_transmit(g_spi, (spi_transaction_t*)&t); + cs_high(); +} + +static inline void tx_cmd_noarg(uint8_t cmd) { tx_cmd(cmd, nullptr, 0); } + +// Stream a chunk of pixels with the 0x32 RAMWR/RAMWRC header, QIO data. +// Caller is responsible for CS low at batch start and high at batch end. +static void tx_pixels_first(const uint8_t* buf_bswapped, size_t n_pixels, + bool use_ramwr) { + spi_transaction_ext_t t = {}; + t.base.flags = SPI_TRANS_MODE_QIO; + t.base.cmd = 0x32; + t.base.addr = use_ramwr ? 0x002C00 : 0x003C00; + t.base.tx_buffer = buf_bswapped; + t.base.length = n_pixels * 16; + spi_device_polling_transmit(g_spi, (spi_transaction_t*)&t); +} + +static void tx_pixels_cont(const uint8_t* buf_bswapped, size_t n_pixels) { + spi_transaction_ext_t t = {}; + t.base.flags = SPI_TRANS_MODE_QIO + | SPI_TRANS_VARIABLE_CMD + | SPI_TRANS_VARIABLE_ADDR + | SPI_TRANS_VARIABLE_DUMMY; + t.command_bits = 0; t.address_bits = 0; t.dummy_bits = 0; + t.base.tx_buffer = buf_bswapped; + t.base.length = n_pixels * 16; + spi_device_polling_transmit(g_spi, (spi_transaction_t*)&t); +} + +// --- Bus init --- +static bool bus_init() { + gpio_set_direction(PIN_CS, GPIO_MODE_OUTPUT); + gpio_set_level(PIN_CS, 1); + + spi_bus_config_t bcfg = {}; + bcfg.mosi_io_num = PIN_D0; + bcfg.miso_io_num = PIN_D1; + bcfg.sclk_io_num = PIN_SCK; + bcfg.quadwp_io_num = PIN_D2; + bcfg.quadhd_io_num = PIN_D3; + bcfg.max_transfer_sz = CHUNK_PIXELS * 2 + 16; + bcfg.flags = SPICOMMON_BUSFLAG_MASTER | SPICOMMON_BUSFLAG_GPIO_PINS; + esp_err_t r = spi_bus_initialize(SPI2_HOST, &bcfg, SPI_DMA_CH_AUTO); + Serial.printf("[bus] spi_bus_initialize -> %d\n", r); + if (r != ESP_OK) return false; + + spi_device_interface_config_t dcfg = {}; + dcfg.command_bits = 8; + dcfg.address_bits = 24; + dcfg.dummy_bits = 0; + dcfg.mode = 3; // vendor + dcfg.cs_ena_pretrans = 0; + dcfg.cs_ena_posttrans = 0; + dcfg.clock_speed_hz = 40 * 1000 * 1000; // vendor pclk + dcfg.spics_io_num = -1; + dcfg.queue_size = 1; + dcfg.flags = SPI_DEVICE_HALFDUPLEX; + r = spi_bus_add_device(SPI2_HOST, &dcfg, &g_spi); + Serial.printf("[bus] spi_bus_add_device -> %d (handle=%p)\n", r, g_spi); + if (r != ESP_OK) return false; + + g_dma = (uint8_t*)heap_caps_aligned_alloc(16, CHUNK_PIXELS * 2, MALLOC_CAP_DMA); + Serial.printf("[bus] dma_buf=%p\n", g_dma); + return g_dma != nullptr; +} + +// --- Vendor init table (from JC3248W535 skill: vendor_specific_init_default) --- +struct InitOp { uint8_t cmd; const uint8_t* data; uint16_t n; uint16_t delay_ms; }; + +static void apply_vendor_init_table() { + static const uint8_t d_BB1[] = {0x00,0x00,0x00,0x00,0x00,0x00,0x5A,0xA5}; + static const uint8_t d_A0[] = {0x00,0x10,0x00,0x02,0x00,0x00,0x64,0x3F,0x20,0x05,0x3F,0x3F,0x00,0x00,0x00,0x00,0x00}; + static const uint8_t d_A2[] = {0x30,0x04,0x0A,0x3C,0xEC,0x54,0xC4,0x30,0xAC,0x28,0x7F,0x7F,0x7F,0x20,0xF8,0x10,0x02,0xFF,0xFF,0xF0,0x90,0x01,0x32,0xA0,0x91,0xC0,0x20,0x7F,0xFF,0x00,0x54}; + static const uint8_t d_D0[] = {0x30,0xAC,0x21,0x24,0x08,0x09,0x10,0x01,0xAA,0x14,0xC2,0x00,0x22,0x22,0xAA,0x03,0x10,0x12,0x40,0x14,0x1E,0x51,0x15,0x00,0x40,0x10,0x00,0x03,0x3D,0x12}; + static const uint8_t d_A3[] = {0xA0,0x06,0xAA,0x08,0x08,0x02,0x0A,0x04,0x04,0x04,0x04,0x04,0x04,0x04,0x04,0x04,0x04,0x04,0x04,0x00,0x55,0x55}; + static const uint8_t d_C1[] = {0x33,0x04,0x02,0x02,0x71,0x05,0x24,0x55,0x02,0x00,0x41,0x00,0x53,0xFF,0xFF,0xFF,0x4F,0x52,0x00,0x4F,0x52,0x00,0x45,0x3B,0x0B,0x02,0x0D,0x00,0xFF,0x40}; + static const uint8_t d_C3[] = {0x00,0x00,0x00,0x50,0x03,0x00,0x00,0x00,0x01,0x80,0x01}; + static const uint8_t d_C4[] = {0x00,0x24,0x33,0x90,0x50,0xEA,0x64,0x32,0xC8,0x64,0xC8,0x32,0x90,0x90,0x11,0x06,0xDC,0xFA,0x04,0x03,0x80,0xFE,0x10,0x10,0x00,0x0A,0x0A,0x44,0x50}; + static const uint8_t d_C5[] = {0x18,0x00,0x00,0x03,0xFE,0x78,0x33,0x20,0x30,0x10,0x88,0xDE,0x0D,0x08,0x0F,0x0F,0x01,0x78,0x33,0x20,0x10,0x10,0x80}; + static const uint8_t d_C6[] = {0x05,0x0A,0x05,0x0A,0x00,0xE0,0x2E,0x0B,0x12,0x22,0x12,0x22,0x01,0x00,0x00,0x3F,0x6A,0x18,0xC8,0x22}; + static const uint8_t d_C7[] = {0x50,0x32,0x28,0x00,0xA2,0x80,0x8F,0x00,0x80,0xFF,0x07,0x11,0x9F,0x6F,0xFF,0x26,0x0C,0x0D,0x0E,0x0F}; + static const uint8_t d_C9[] = {0x33,0x44,0x44,0x01}; + static const uint8_t d_CF[] = {0x34,0x1E,0x88,0x58,0x13,0x18,0x56,0x18,0x1E,0x68,0xF7,0x00,0x65,0x0C,0x22,0xC4,0x0C,0x77,0x22,0x44,0xAA,0x55,0x04,0x04,0x12,0xA0,0x08}; + static const uint8_t d_D5[] = {0x3E,0x3E,0x88,0x00,0x44,0x04,0x78,0x33,0x20,0x78,0x33,0x20,0x04,0x28,0xD3,0x47,0x03,0x03,0x03,0x03,0x86,0x00,0x00,0x00,0x30,0x52,0x3F,0x40,0x40,0x96}; + static const uint8_t d_D6[] = {0x10,0x32,0x54,0x76,0x98,0xBA,0xDC,0xFE,0x95,0x00,0x01,0x83,0x75,0x36,0x20,0x75,0x36,0x20,0x3F,0x03,0x03,0x03,0x10,0x10,0x00,0x04,0x51,0x20,0x01,0x00}; + static const uint8_t d_D7[] = {0x0A,0x08,0x0E,0x0C,0x1E,0x18,0x19,0x1F,0x00,0x1F,0x1A,0x1F,0x3E,0x3E,0x04,0x00,0x1F,0x1F,0x1F}; + static const uint8_t d_D8[] = {0x0B,0x09,0x0F,0x0D,0x1E,0x18,0x19,0x1F,0x01,0x1F,0x1A,0x1F}; + static const uint8_t d_D9[] = {0x00,0x0D,0x0F,0x09,0x0B,0x1F,0x18,0x19,0x1F,0x01,0x1E,0x1A,0x1F}; + static const uint8_t d_DD[] = {0x0C,0x0E,0x08,0x0A,0x1F,0x18,0x19,0x1F,0x00,0x1E,0x1A,0x1F}; + static const uint8_t d_DF[] = {0x44,0x73,0x4B,0x69,0x00,0x0A,0x02,0x90}; + static const uint8_t d_E0[] = {0x19,0x20,0x0A,0x13,0x0E,0x09,0x12,0x28,0xD4,0x24,0x0C,0x35,0x13,0x31,0x36,0x2F,0x03}; + static const uint8_t d_E1[] = {0x38,0x20,0x09,0x12,0x0E,0x08,0x12,0x28,0xC5,0x24,0x0C,0x34,0x12,0x31,0x36,0x2F,0x27}; + static const uint8_t d_E2[] = {0x19,0x20,0x0A,0x11,0x09,0x06,0x11,0x25,0xD4,0x22,0x0B,0x33,0x12,0x2D,0x32,0x2F,0x03}; + static const uint8_t d_E3[] = {0x38,0x20,0x0A,0x11,0x09,0x06,0x11,0x25,0xC4,0x21,0x0A,0x32,0x11,0x2C,0x32,0x2F,0x27}; + static const uint8_t d_E4[] = {0x19,0x20,0x0D,0x14,0x0D,0x08,0x12,0x2A,0xD4,0x26,0x0E,0x35,0x13,0x34,0x39,0x2F,0x03}; + static const uint8_t d_E5[] = {0x38,0x20,0x0D,0x13,0x0D,0x07,0x12,0x29,0xC4,0x25,0x0D,0x35,0x12,0x33,0x39,0x2F,0x27}; + static const uint8_t d_BB2[] = {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}; + static const uint8_t d_2C[] = {0x00,0x00,0x00,0x00}; + + static const InitOp ops[] = { + {0xBB, d_BB1, sizeof(d_BB1), 0}, {0xA0, d_A0, sizeof(d_A0), 0}, + {0xA2, d_A2, sizeof(d_A2), 0}, {0xD0, d_D0, sizeof(d_D0), 0}, + {0xA3, d_A3, sizeof(d_A3), 0}, {0xC1, d_C1, sizeof(d_C1), 0}, + {0xC3, d_C3, sizeof(d_C3), 0}, {0xC4, d_C4, sizeof(d_C4), 0}, + {0xC5, d_C5, sizeof(d_C5), 0}, {0xC6, d_C6, sizeof(d_C6), 0}, + {0xC7, d_C7, sizeof(d_C7), 0}, {0xC9, d_C9, sizeof(d_C9), 0}, + {0xCF, d_CF, sizeof(d_CF), 0}, {0xD5, d_D5, sizeof(d_D5), 0}, + {0xD6, d_D6, sizeof(d_D6), 0}, {0xD7, d_D7, sizeof(d_D7), 0}, + {0xD8, d_D8, sizeof(d_D8), 0}, {0xD9, d_D9, sizeof(d_D9), 0}, + {0xDD, d_DD, sizeof(d_DD), 0}, {0xDF, d_DF, sizeof(d_DF), 0}, + {0xE0, d_E0, sizeof(d_E0), 0}, {0xE1, d_E1, sizeof(d_E1), 0}, + {0xE2, d_E2, sizeof(d_E2), 0}, {0xE3, d_E3, sizeof(d_E3), 0}, + {0xE4, d_E4, sizeof(d_E4), 0}, {0xE5, d_E5, sizeof(d_E5), 0}, + {0xBB, d_BB2, sizeof(d_BB2), 0}, + {0x13, nullptr, 0, 0}, // NORON + {0x11, nullptr, 0, 200}, // SLPOUT + 200ms + {0x29, nullptr, 0, 200}, // DISPON + 200ms + {0x2C, d_2C, sizeof(d_2C), 0}, // vendor trailing RAMWR param + }; + + spi_device_acquire_bus(g_spi, portMAX_DELAY); + for (auto& e : ops) { + tx_cmd(e.cmd, e.data, e.n); + if (e.delay_ms) delay(e.delay_ms); + } + spi_device_release_bus(g_spi); +} + +static void panel_init() { + spi_device_acquire_bus(g_spi, portMAX_DELAY); + tx_cmd_noarg(0x01); // SWRESET — no HW reset pin on this board + delay(200); + uint8_t madctl = 0x00; + tx_cmd(0x36, &madctl, 1); + uint8_t colmod = 0x55; // RGB565 per AXS encoding + tx_cmd(0x3A, &colmod, 1); + spi_device_release_bus(g_spi); + + apply_vendor_init_table(); // includes its own SLPOUT+DISPON +} + +// --- Draw: fill whole panel with one color --- +// CASET to full width, skip RASET (QSPI rule), then stream pixels until we've +// painted W*H. Pixel order in RAM is row-major from CASET top-left. +static void fill_screen(uint16_t color565) { + uint16_t sw = __builtin_bswap16(color565); + + spi_device_acquire_bus(g_spi, portMAX_DELAY); + + uint8_t col[4] = { 0, 0, (uint8_t)((LCD_W - 1) >> 8), (uint8_t)((LCD_W - 1) & 0xFF) }; + tx_cmd(0x2A, col, 4); + + // Preload DMA buffer with the swapped color (CHUNK_PIXELS at a time) + uint16_t* dst = (uint16_t*)g_dma; + for (size_t i = 0; i < CHUNK_PIXELS; ++i) dst[i] = sw; + + size_t total = (size_t)LCD_W * LCD_H; + bool first = true; + cs_low(); + while (total) { + size_t n = total > CHUNK_PIXELS ? CHUNK_PIXELS : total; + if (first) { tx_pixels_first(g_dma, n, /*use_ramwr=*/true); first = false; } + else { tx_pixels_cont(g_dma, n); } + total -= n; + } + cs_high(); + + spi_device_release_bus(g_spi); +} + +// --- Setup / loop --- +void setup() { + Serial.begin(115200); + delay(300); + Serial.println("\n=== BARE-METAL AXS15231B DIAGNOSTIC ==="); + + pinMode(PIN_BL, OUTPUT); + digitalWrite(PIN_BL, HIGH); + Serial.println("[bl] backlight on"); + + Wire.begin(PIN_SDA, PIN_SCL); + Wire.setClock(400000); + + if (!bus_init()) { Serial.println("bus_init FAILED"); return; } + Serial.println("[bus] ready"); + + panel_init(); + Serial.println("[panel] init done"); + + Serial.println("fill_screen RED"); + fill_screen(0xF800); + delay(1500); + + Serial.println("fill_screen GREEN"); + fill_screen(0x07E0); + delay(1500); + + Serial.println("fill_screen BLUE"); + fill_screen(0x001F); + delay(1500); + + Serial.println("fill_screen WHITE"); + fill_screen(0xFFFF); + delay(1500); + + Serial.println("fill_screen BLACK"); + fill_screen(0x0000); + + Serial.println("Diagnostic halted"); +} + +static bool axs_touch_read() { + static const uint8_t read_cmd[11] = { + 0xB5, 0xAB, 0xA5, 0x5A, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00 + }; + Wire.beginTransmission(0x3B); + Wire.write(read_cmd, sizeof(read_cmd)); + if (Wire.endTransmission() != 0) return false; + uint8_t buf[8] = {0}; + size_t got = Wire.requestFrom((uint8_t)0x3B, (uint8_t)8); + if (got < 6) return false; + for (size_t i = 0; i < got && i < sizeof(buf); ++i) buf[i] = Wire.read(); + return (buf[0] == 0) && ((buf[1] & 0x0F) != 0); +} + +void loop() { + if (axs_touch_read()) { + Serial.println("Touch detected — restarting"); + delay(50); + ESP.restart(); + } + delay(30); +} diff --git a/src/vendor/esp_lcd_axs15231b.c b/src/vendor/esp_lcd_axs15231b.c new file mode 100644 index 0000000..edf304d --- /dev/null +++ b/src/vendor/esp_lcd_axs15231b.c @@ -0,0 +1,392 @@ +#ifdef BOARD_IS_JC3248W535_VENDOR +/* + * SPDX-FileCopyrightText: 2021-2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_lcd_panel_interface.h" +#include "esp_lcd_panel_io.h" +#include "esp_lcd_panel_vendor.h" +#include "esp_lcd_panel_ops.h" +#include "esp_lcd_panel_commands.h" +#include "driver/gpio.h" +#include "esp_log.h" +#include "esp_check.h" +#include "esp_lcd_touch.h" + +#include "esp_lcd_axs15231b.h" + +/*max point num*/ +#define AXS_MAX_TOUCH_NUMBER (1) + +#define LCD_OPCODE_WRITE_CMD (0x02ULL) +#define LCD_OPCODE_READ_CMD (0x0BULL) +#define LCD_OPCODE_WRITE_COLOR (0x32ULL) + +static const char *TAG = "lcd_panel.axs15231b"; + +static esp_err_t panel_axs15231b_del(esp_lcd_panel_t *panel); +static esp_err_t panel_axs15231b_reset(esp_lcd_panel_t *panel); +static esp_err_t panel_axs15231b_init(esp_lcd_panel_t *panel); +static esp_err_t panel_axs15231b_draw_bitmap(esp_lcd_panel_t *panel, int x_start, int y_start, int x_end, int y_end, const void *color_data); +static esp_err_t panel_axs15231b_invert_color(esp_lcd_panel_t *panel, bool invert_color_data); +static esp_err_t panel_axs15231b_mirror(esp_lcd_panel_t *panel, bool mirror_x, bool mirror_y); +static esp_err_t panel_axs15231b_swap_xy(esp_lcd_panel_t *panel, bool swap_axes); +static esp_err_t panel_axs15231b_set_gap(esp_lcd_panel_t *panel, int x_gap, int y_gap); +static esp_err_t panel_axs15231b_disp_off(esp_lcd_panel_t *panel, bool off); + +// Touch forward declarations and implementation removed — this build is +// display-only. Touch is driven by a separate I2C path in main_vendor.cpp. + +typedef struct { + esp_lcd_panel_t base; + esp_lcd_panel_io_handle_t io; + int reset_gpio_num; + int x_gap; + int y_gap; + uint8_t fb_bits_per_pixel; + uint8_t madctl_val; // save current value of LCD_CMD_MADCTL register + uint8_t colmod_val; // save surrent value of LCD_CMD_COLMOD register + const axs15231b_lcd_init_cmd_t *init_cmds; + uint16_t init_cmds_size; + struct { + unsigned int use_qspi_interface: 1; + unsigned int reset_level: 1; + } flags; +} axs15231b_panel_t; + +esp_err_t esp_lcd_new_panel_axs15231b(const esp_lcd_panel_io_handle_t io, const esp_lcd_panel_dev_config_t *panel_dev_config, esp_lcd_panel_handle_t *ret_panel) +{ + esp_err_t ret = ESP_OK; + axs15231b_panel_t *axs15231b = NULL; + ESP_GOTO_ON_FALSE(io && panel_dev_config && ret_panel, ESP_ERR_INVALID_ARG, err, TAG, "invalid argument"); + axs15231b = calloc(1, sizeof(axs15231b_panel_t)); + ESP_GOTO_ON_FALSE(axs15231b, ESP_ERR_NO_MEM, err, TAG, "no mem for axs15231b panel"); + + if (panel_dev_config->reset_gpio_num >= 0) { + gpio_config_t io_conf = { + .mode = GPIO_MODE_OUTPUT, + .pin_bit_mask = 1ULL << panel_dev_config->reset_gpio_num, + }; + ESP_GOTO_ON_ERROR(gpio_config(&io_conf), err, TAG, "configure GPIO for RST line failed"); + } + + switch (panel_dev_config->color_space) { + case LCD_RGB_ELEMENT_ORDER_RGB: + axs15231b->madctl_val = 0; + break; + case LCD_RGB_ELEMENT_ORDER_BGR: + axs15231b->madctl_val |= LCD_CMD_BGR_BIT; + break; + default: + ESP_GOTO_ON_FALSE(false, ESP_ERR_NOT_SUPPORTED, err, TAG, "unsupported RGB element order"); + break; + } + + uint8_t fb_bits_per_pixel = 0; + switch (panel_dev_config->bits_per_pixel) { + case 16: // RGB565 + axs15231b->colmod_val = 0x55; + fb_bits_per_pixel = 16; + break; + case 18: // RGB666 + axs15231b->colmod_val = 0x66; + // each color component (R/G/B) should occupy the 6 high bits of a byte, which means 3 full bytes are required for a pixel + fb_bits_per_pixel = 24; + break; + default: + ESP_GOTO_ON_FALSE(false, ESP_ERR_NOT_SUPPORTED, err, TAG, "unsupported pixel width"); + break; + } + + axs15231b->io = io; + axs15231b->fb_bits_per_pixel = fb_bits_per_pixel; + axs15231b->reset_gpio_num = panel_dev_config->reset_gpio_num; + axs15231b->flags.reset_level = panel_dev_config->flags.reset_active_high; + if (panel_dev_config->vendor_config) { + axs15231b->init_cmds = ((axs15231b_vendor_config_t *)panel_dev_config->vendor_config)->init_cmds; + axs15231b->init_cmds_size = ((axs15231b_vendor_config_t *)panel_dev_config->vendor_config)->init_cmds_size; + axs15231b->flags.use_qspi_interface = ((axs15231b_vendor_config_t *)panel_dev_config->vendor_config)->flags.use_qspi_interface; + } + axs15231b->base.del = panel_axs15231b_del; + axs15231b->base.reset = panel_axs15231b_reset; + axs15231b->base.init = panel_axs15231b_init; + axs15231b->base.draw_bitmap = panel_axs15231b_draw_bitmap; + axs15231b->base.invert_color = panel_axs15231b_invert_color; + axs15231b->base.set_gap = panel_axs15231b_set_gap; + axs15231b->base.mirror = panel_axs15231b_mirror; + axs15231b->base.swap_xy = panel_axs15231b_swap_xy; + axs15231b->base.disp_off = panel_axs15231b_disp_off; // arduino-esp32 3.0.x/ESP-IDF 5.1 struct field name + *ret_panel = &(axs15231b->base); + ESP_LOGD(TAG, "new axs15231b panel @%p", axs15231b); + ESP_LOGI(TAG, "LCD panel create success, version: %d.%d.%d", ESP_LCD_AXS15231B_VER_MAJOR, ESP_LCD_AXS15231B_VER_MINOR, + ESP_LCD_AXS15231B_VER_PATCH); + + return ESP_OK; + +err: + if (axs15231b) { + if (panel_dev_config->reset_gpio_num >= 0) { + gpio_reset_pin(panel_dev_config->reset_gpio_num); + } + free(axs15231b); + } + return ret; +} + +static esp_err_t tx_param(axs15231b_panel_t *axs15231b, esp_lcd_panel_io_handle_t io, int lcd_cmd, const void *param, size_t param_size) +{ + if (axs15231b->flags.use_qspi_interface) { + lcd_cmd &= 0xff; + lcd_cmd <<= 8; + lcd_cmd |= LCD_OPCODE_WRITE_CMD << 24; + } + return esp_lcd_panel_io_tx_param(io, lcd_cmd, param, param_size); +} + +static esp_err_t tx_color(axs15231b_panel_t *axs15231b, esp_lcd_panel_io_handle_t io, int lcd_cmd, const void *param, size_t param_size) +{ + if (axs15231b->flags.use_qspi_interface) { + lcd_cmd &= 0xff; + lcd_cmd <<= 8; + lcd_cmd |= LCD_OPCODE_WRITE_COLOR << 24; + } + return esp_lcd_panel_io_tx_color(io, lcd_cmd, param, param_size); +} + +static esp_err_t panel_axs15231b_del(esp_lcd_panel_t *panel) +{ + axs15231b_panel_t *axs15231b = __containerof(panel, axs15231b_panel_t, base); + + if (axs15231b->reset_gpio_num >= 0) { + gpio_reset_pin(axs15231b->reset_gpio_num); + } + ESP_LOGD(TAG, "del axs15231b panel @%p", axs15231b); + free(axs15231b); + return ESP_OK; +} + +static esp_err_t panel_axs15231b_reset(esp_lcd_panel_t *panel) +{ + axs15231b_panel_t *axs15231b = __containerof(panel, axs15231b_panel_t, base); + esp_lcd_panel_io_handle_t io = axs15231b->io; + + // perform hardware reset + if (axs15231b->reset_gpio_num >= 0) { + gpio_set_level(axs15231b->reset_gpio_num, !axs15231b->flags.reset_level); + vTaskDelay(pdMS_TO_TICKS(10)); + gpio_set_level(axs15231b->reset_gpio_num, axs15231b->flags.reset_level); + vTaskDelay(pdMS_TO_TICKS(10)); + gpio_set_level(axs15231b->reset_gpio_num, !axs15231b->flags.reset_level); + vTaskDelay(pdMS_TO_TICKS(120)); + } else { // perform software reset + tx_param(axs15231b, io, LCD_CMD_SWRESET, NULL, 0); + vTaskDelay(pdMS_TO_TICKS(120)); // spec, wait at least 5m before sending new command + } + + return ESP_OK; +} + +static const axs15231b_lcd_init_cmd_t vendor_specific_init_default[] = { + {0xBB, (uint8_t[]){0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5A, 0xA5}, 8, 0}, + {0xA0, (uint8_t[]){0x00, 0x10, 0x00, 0x02, 0x00, 0x00, 0x64, 0x3F, 0x20, 0x05, 0x3F, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00}, 17, 0}, + {0xA2, (uint8_t[]){0x30, 0x04, 0x0A, 0x3C, 0xEC, 0x54, 0xC4, 0x30, 0xAC, 0x28, 0x7F, 0x7F, 0x7F, 0x20, 0xF8, 0x10, 0x02, 0xFF, 0xFF, 0xF0, 0x90, 0x01, 0x32, 0xA0, 0x91, 0xC0, 0x20, 0x7F, 0xFF, 0x00, 0x54}, 31, 0}, + {0xD0, (uint8_t[]){0x30, 0xAC, 0x21, 0x24, 0x08, 0x09, 0x10, 0x01, 0xAA, 0x14, 0xC2, 0x00, 0x22, 0x22, 0xAA, 0x03, 0x10, 0x12, 0x40, 0x14, 0x1E, 0x51, 0x15, 0x00, 0x40, 0x10, 0x00, 0x03, 0x3D, 0x12}, 30, 0}, + {0xA3, (uint8_t[]){0xA0, 0x06, 0xAA, 0x08, 0x08, 0x02, 0x0A, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x00, 0x55, 0x55}, 22, 0}, + {0xC1, (uint8_t[]){0x33, 0x04, 0x02, 0x02, 0x71, 0x05, 0x24, 0x55, 0x02, 0x00, 0x41, 0x00, 0x53, 0xFF, 0xFF, 0xFF, 0x4F, 0x52, 0x00, 0x4F, 0x52, 0x00, 0x45, 0x3B, 0x0B, 0x02, 0x0D, 0x00, 0xFF, 0x40}, 30, 0}, + {0xC3, (uint8_t[]){0x00, 0x00, 0x00, 0x50, 0x03, 0x00, 0x00, 0x00, 0x01, 0x80, 0x01}, 11, 0}, + {0xC4, (uint8_t[]){0x00, 0x24, 0x33, 0x90, 0x50, 0xea, 0x64, 0x32, 0xC8, 0x64, 0xC8, 0x32, 0x90, 0x90, 0x11, 0x06, 0xDC, 0xFA, 0x04, 0x03, 0x80, 0xFE, 0x10, 0x10, 0x00, 0x0A, 0x0A, 0x44, 0x50}, 29, 0}, + {0xC5, (uint8_t[]){0x18, 0x00, 0x00, 0x03, 0xFE, 0x78, 0x33, 0x20, 0x30, 0x10, 0x88, 0xDE, 0x0D, 0x08, 0x0F, 0x0F, 0x01, 0x78, 0x33, 0x20, 0x10, 0x10, 0x80}, 23, 0}, + {0xC6, (uint8_t[]){0x05, 0x0A, 0x05, 0x0A, 0x00, 0xE0, 0x2E, 0x0B, 0x12, 0x22, 0x12, 0x22, 0x01, 0x00, 0x00, 0x3F, 0x6A, 0x18, 0xC8, 0x22}, 20, 0}, + {0xC7, (uint8_t[]){0x50, 0x32, 0x28, 0x00, 0xa2, 0x80, 0x8f, 0x00, 0x80, 0xff, 0x07, 0x11, 0x9F, 0x6f, 0xff, 0x26, 0x0c, 0x0d, 0x0e, 0x0f}, 20, 0}, + {0xC9, (uint8_t[]){0x33, 0x44, 0x44, 0x01}, 4, 0}, + {0xCF, (uint8_t[]){0x34, 0x1E, 0x88, 0x58, 0x13, 0x18, 0x56, 0x18, 0x1E, 0x68, 0xF7, 0x00, 0x65, 0x0C, 0x22, 0xC4, 0x0C, 0x77, 0x22, 0x44, 0xAA, 0x55, 0x04, 0x04, 0x12, 0xA0, 0x08}, 27, 0}, + {0xD5, (uint8_t[]){0x3E, 0x3E, 0x88, 0x00, 0x44, 0x04, 0x78, 0x33, 0x20, 0x78, 0x33, 0x20, 0x04, 0x28, 0xD3, 0x47, 0x03, 0x03, 0x03, 0x03, 0x86, 0x00, 0x00, 0x00, 0x30, 0x52, 0x3f, 0x40, 0x40, 0x96}, 30, 0}, + {0xD6, (uint8_t[]){0x10, 0x32, 0x54, 0x76, 0x98, 0xBA, 0xDC, 0xFE, 0x95, 0x00, 0x01, 0x83, 0x75, 0x36, 0x20, 0x75, 0x36, 0x20, 0x3F, 0x03, 0x03, 0x03, 0x10, 0x10, 0x00, 0x04, 0x51, 0x20, 0x01, 0x00}, 30, 0}, + {0xD7, (uint8_t[]){0x0a, 0x08, 0x0e, 0x0c, 0x1E, 0x18, 0x19, 0x1F, 0x00, 0x1F, 0x1A, 0x1F, 0x3E, 0x3E, 0x04, 0x00, 0x1F, 0x1F, 0x1F}, 19, 0}, + {0xD8, (uint8_t[]){0x0B, 0x09, 0x0F, 0x0D, 0x1E, 0x18, 0x19, 0x1F, 0x01, 0x1F, 0x1A, 0x1F}, 12, 0}, + {0xD9, (uint8_t[]){0x00, 0x0D, 0x0F, 0x09, 0x0B, 0x1F, 0x18, 0x19, 0x1F, 0x01, 0x1E, 0x1A, 0x1F}, 13, 0}, + {0xDD, (uint8_t[]){0x0C, 0x0E, 0x08, 0x0A, 0x1F, 0x18, 0x19, 0x1F, 0x00, 0x1E, 0x1A, 0x1F}, 12, 0}, + {0xDF, (uint8_t[]){0x44, 0x73, 0x4B, 0x69, 0x00, 0x0A, 0x02, 0x90}, 8, 0}, + {0xE0, (uint8_t[]){0x19, 0x20, 0x0A, 0x13, 0x0E, 0x09, 0x12, 0x28, 0xD4, 0x24, 0x0C, 0x35, 0x13, 0x31, 0x36, 0x2f, 0x03}, 17, 0}, + {0xE1, (uint8_t[]){0x38, 0x20, 0x09, 0x12, 0x0E, 0x08, 0x12, 0x28, 0xC5, 0x24, 0x0C, 0x34, 0x12, 0x31, 0x36, 0x2f, 0x27}, 17, 0}, + {0xE2, (uint8_t[]){0x19, 0x20, 0x0A, 0x11, 0x09, 0x06, 0x11, 0x25, 0xD4, 0x22, 0x0B, 0x33, 0x12, 0x2D, 0x32, 0x2f, 0x03}, 17, 0}, + {0xE3, (uint8_t[]){0x38, 0x20, 0x0A, 0x11, 0x09, 0x06, 0x11, 0x25, 0xC4, 0x21, 0x0A, 0x32, 0x11, 0x2C, 0x32, 0x2f, 0x27}, 17, 0}, + {0xE4, (uint8_t[]){0x19, 0x20, 0x0D, 0x14, 0x0D, 0x08, 0x12, 0x2A, 0xD4, 0x26, 0x0E, 0x35, 0x13, 0x34, 0x39, 0x2f, 0x03}, 17, 0}, + {0xE5, (uint8_t[]){0x38, 0x20, 0x0D, 0x13, 0x0D, 0x07, 0x12, 0x29, 0xC4, 0x25, 0x0D, 0x35, 0x12, 0x33, 0x39, 0x2f, 0x27}, 17, 0}, + {0xBB, (uint8_t[]){0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, 8, 0}, + {0x13, (uint8_t[]){0x00}, 0, 0}, + {0x11, (uint8_t[]){0x00}, 0, 200}, + {0x29, (uint8_t[]){0x00}, 0, 200}, + {0x2C, (uint8_t[]){0x00, 0x00, 0x00, 0x00}, 4, 0}, + {0x22, (uint8_t[]){0x00}, 0, 200},//All Pixels off +}; + +static esp_err_t panel_axs15231b_init(esp_lcd_panel_t *panel) +{ + axs15231b_panel_t *axs15231b = __containerof(panel, axs15231b_panel_t, base); + esp_lcd_panel_io_handle_t io = axs15231b->io; + + // LCD goes into sleep mode and display will be turned off after power on reset, exit sleep mode first + ESP_RETURN_ON_ERROR(tx_param(axs15231b, io, LCD_CMD_SLPOUT, NULL, 0), TAG, "send command failed"); + vTaskDelay(pdMS_TO_TICKS(100)); + ESP_RETURN_ON_ERROR(tx_param(axs15231b, io, LCD_CMD_MADCTL, (uint8_t[]) { + axs15231b->madctl_val, + }, 1), TAG, "send command failed"); + ESP_RETURN_ON_ERROR(tx_param(axs15231b, io, LCD_CMD_COLMOD, (uint8_t[]) { + axs15231b->colmod_val, + }, 1), TAG, "send command failed"); + + const axs15231b_lcd_init_cmd_t *init_cmds = NULL; + uint16_t init_cmds_size = 0; + if (axs15231b->init_cmds) { + init_cmds = axs15231b->init_cmds; + init_cmds_size = axs15231b->init_cmds_size; + } else { + init_cmds = vendor_specific_init_default; + init_cmds_size = sizeof(vendor_specific_init_default) / sizeof(axs15231b_lcd_init_cmd_t); + } + + bool is_cmd_overwritten = false; + for (int i = 0; i < init_cmds_size; i++) { + // Check if the command has been used or conflicts with the internal + switch (init_cmds[i].cmd) { + case LCD_CMD_MADCTL: + is_cmd_overwritten = true; + axs15231b->madctl_val = ((uint8_t *)init_cmds[i].data)[0]; + break; + case LCD_CMD_COLMOD: + is_cmd_overwritten = true; + axs15231b->colmod_val = ((uint8_t *)init_cmds[i].data)[0]; + break; + default: + is_cmd_overwritten = false; + break; + } + + if (is_cmd_overwritten) { + ESP_LOGW(TAG, "The %02Xh command has been used and will be overwritten by external initialization sequence", init_cmds[i].cmd); + } + ESP_RETURN_ON_ERROR(tx_param(axs15231b, io, init_cmds[i].cmd, init_cmds[i].data, init_cmds[i].data_bytes), TAG, "send command failed"); + vTaskDelay(pdMS_TO_TICKS(init_cmds[i].delay_ms)); + } + ESP_LOGI(TAG, "send init commands success"); + + return ESP_OK; +} + +static esp_err_t panel_axs15231b_draw_bitmap(esp_lcd_panel_t *panel, int x_start, int y_start, int x_end, int y_end, const void *color_data) +{ + axs15231b_panel_t *axs15231b = __containerof(panel, axs15231b_panel_t, base); + assert((x_start < x_end) && (y_start < y_end) && "start position must be smaller than end position"); + esp_lcd_panel_io_handle_t io = axs15231b->io; + + x_start += axs15231b->x_gap; + x_end += axs15231b->x_gap; + y_start += axs15231b->y_gap; + y_end += axs15231b->y_gap; + + // define an area of frame memory where MCU can access + tx_param(axs15231b, io, LCD_CMD_CASET, (uint8_t[]) { + (x_start >> 8) & 0xFF, + x_start & 0xFF, + ((x_end - 1) >> 8) & 0xFF, + (x_end - 1) & 0xFF, + }, 4); + + if (0 == axs15231b->flags.use_qspi_interface) { + tx_param(axs15231b, io, LCD_CMD_RASET, (uint8_t[]) { + (y_start >> 8) & 0xFF, + y_start & 0xFF, + ((y_end - 1) >> 8) & 0xFF, + (y_end - 1) & 0xFF, + }, 4); + } + + // transfer frame buffer + size_t len = (x_end - x_start) * (y_end - y_start) * axs15231b->fb_bits_per_pixel / 8; + if (y_start == 0) { + tx_color(axs15231b, io, LCD_CMD_RAMWR, color_data, len);//2C + } else { + tx_color(axs15231b, io, LCD_CMD_RAMWRC, color_data, len);//3C + } + + return ESP_OK; +} + +static esp_err_t panel_axs15231b_invert_color(esp_lcd_panel_t *panel, bool invert_color_data) +{ + axs15231b_panel_t *axs15231b = __containerof(panel, axs15231b_panel_t, base); + esp_lcd_panel_io_handle_t io = axs15231b->io; + int command = 0; + if (invert_color_data) { + command = LCD_CMD_INVON; + } else { + command = LCD_CMD_INVOFF; + } + tx_param(axs15231b, io, command, NULL, 0); + return ESP_OK; +} + +static esp_err_t panel_axs15231b_mirror(esp_lcd_panel_t *panel, bool mirror_x, bool mirror_y) +{ + axs15231b_panel_t *axs15231b = __containerof(panel, axs15231b_panel_t, base); + esp_lcd_panel_io_handle_t io = axs15231b->io; + if (mirror_x) { + axs15231b->madctl_val |= LCD_CMD_MX_BIT; + } else { + axs15231b->madctl_val &= ~LCD_CMD_MX_BIT; + } + if (mirror_y) { + axs15231b->madctl_val |= LCD_CMD_MY_BIT; + } else { + axs15231b->madctl_val &= ~LCD_CMD_MY_BIT; + } + tx_param(axs15231b, io, LCD_CMD_MADCTL, (uint8_t[]) { + axs15231b->madctl_val + }, 1); + return ESP_OK; +} + +static esp_err_t panel_axs15231b_swap_xy(esp_lcd_panel_t *panel, bool swap_axes) +{ + axs15231b_panel_t *axs15231b = __containerof(panel, axs15231b_panel_t, base); + esp_lcd_panel_io_handle_t io = axs15231b->io; + if (swap_axes) { + axs15231b->madctl_val |= LCD_CMD_MV_BIT; + } else { + axs15231b->madctl_val &= ~LCD_CMD_MV_BIT; + } + tx_param(axs15231b, io, LCD_CMD_MADCTL, (uint8_t[]) { + axs15231b->madctl_val + }, 1); + return ESP_OK; +} + +static esp_err_t panel_axs15231b_set_gap(esp_lcd_panel_t *panel, int x_gap, int y_gap) +{ + axs15231b_panel_t *axs15231b = __containerof(panel, axs15231b_panel_t, base); + axs15231b->x_gap = x_gap; + axs15231b->y_gap = y_gap; + return ESP_OK; +} + +static esp_err_t panel_axs15231b_disp_off(esp_lcd_panel_t *panel, bool off) +{ + axs15231b_panel_t *axs15231b = __containerof(panel, axs15231b_panel_t, base); + esp_lcd_panel_io_handle_t io = axs15231b->io; + int command = 0; + if (off) { + command = LCD_CMD_DISPOFF; + } else { + command = LCD_CMD_DISPON; + } + tx_param(axs15231b, io, command, NULL, 0); + return ESP_OK; +} +#endif // BOARD_IS_JC3248W535_VENDOR diff --git a/src/vendor/esp_lcd_axs15231b.h b/src/vendor/esp_lcd_axs15231b.h new file mode 100644 index 0000000..4f480de --- /dev/null +++ b/src/vendor/esp_lcd_axs15231b.h @@ -0,0 +1,208 @@ +/* + * SPDX-FileCopyrightText: 2022-2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +/** + * @file + * @brief ESP LCD & Touch: AXS15231B + */ + +#pragma once + +// hal/spi_ll.h removed — not used in this header, and including it from C++ +// breaks due to duplicate operator<<= definitions across multiple FLAG_ATTR +// typedefs in arduino-esp32 3.0.x. +// esp_lcd_touch.h removed — touch API is out of scope for our display-only fork. +#include "esp_lcd_panel_vendor.h" + +// Arduino-esp32 3.0.x ships ESP-IDF 5.1 which uses esp_lcd_color_space_t +// (ESP_LCD_COLOR_SPACE_RGB/BGR). The vendor driver source was written +// against ESP-IDF 5.2+ which renamed these to LCD_RGB_ELEMENT_ORDER_RGB/BGR. +// Define compatibility aliases. +#ifndef LCD_RGB_ELEMENT_ORDER_RGB +#define LCD_RGB_ELEMENT_ORDER_RGB ESP_LCD_COLOR_SPACE_RGB +#define LCD_RGB_ELEMENT_ORDER_BGR ESP_LCD_COLOR_SPACE_BGR +#endif + +#define ESP_LCD_AXS15231B_VER_MAJOR (1) +#define ESP_LCD_AXS15231B_VER_MINOR (0) +#define ESP_LCD_AXS15231B_VER_PATCH (0) + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief LCD panel initialization commands. + * + */ +typedef struct { + int cmd; /* +#include "sdkconfig.h" +#include "esp_err.h" +#include "driver/gpio.h" +#include "esp_lcd_panel_io.h" +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define ESP_LCD_TOUCH_VER_MAJOR (1) +#define ESP_LCD_TOUCH_VER_MINOR (1) +#define ESP_LCD_TOUCH_VER_PATCH (2) + +#define CONFIG_ESP_LCD_TOUCH_MAX_POINTS (1) +#define CONFIG_ESP_LCD_TOUCH_MAX_BUTTONS (0) + +/** + * @brief Touch controller type + * + */ +typedef struct esp_lcd_touch_s esp_lcd_touch_t; +typedef esp_lcd_touch_t *esp_lcd_touch_handle_t; + +/** + * @brief Touch controller interrupt callback type + * + */ +typedef void (*esp_lcd_touch_interrupt_callback_t)(esp_lcd_touch_handle_t tp); + +/** + * @brief Touch Configuration Type + * + */ +typedef struct { + uint16_t x_max; /*!< X coordinates max (for mirroring) */ + uint16_t y_max; /*!< Y coordinates max (for mirroring) */ + + gpio_num_t rst_gpio_num; /*!< GPIO number of reset pin */ + gpio_num_t int_gpio_num; /*!< GPIO number of interrupt pin */ + + struct { + unsigned int reset: 1; /*!< Level of reset pin in reset */ + unsigned int interrupt: 1;/*!< Active Level of interrupt pin */ + } levels; + + struct { + unsigned int swap_xy: 1; /*!< Swap X and Y after read coordinates */ + unsigned int mirror_x: 1; /*!< Mirror X after read coordinates */ + unsigned int mirror_y: 1; /*!< Mirror Y after read coordinates */ + } flags; + + /*!< User callback called after get coordinates from touch controller for apply user adjusting */ + void (*process_coordinates)(esp_lcd_touch_handle_t tp, uint16_t *x, uint16_t *y, uint16_t *strength, uint8_t *point_num, uint8_t max_point_num); + /*!< User callback called after the touch interrupt occurred */ + esp_lcd_touch_interrupt_callback_t interrupt_callback; + /*!< User data passed to callback */ + void *user_data; + /*!< User data passed to driver */ + void *driver_data; +} esp_lcd_touch_config_t; + +typedef struct { + uint8_t points; /*!< Count of touch points saved */ + + struct { + uint16_t x; /*!< X coordinate */ + uint16_t y; /*!< Y coordinate */ + uint16_t strength; /*!< Strength */ + } coords[CONFIG_ESP_LCD_TOUCH_MAX_POINTS]; + +#if (CONFIG_ESP_LCD_TOUCH_MAX_BUTTONS > 0) + uint8_t buttons; /*!< Count of buttons states saved */ + + struct { + uint8_t status; /*!< Status of button */ + } button[CONFIG_ESP_LCD_TOUCH_MAX_BUTTONS]; +#endif + + portMUX_TYPE lock; /*!< Lock for read/write */ +} esp_lcd_touch_data_t; + +/** + * @brief Declare of Touch Type + * + */ +struct esp_lcd_touch_s { + + /** + * @brief set touch controller into sleep mode + * + * @note This function is usually blocking. + * + * @param tp: Touch handler + * + * @return + * - ESP_OK on success, otherwise returns ESP_ERR_xxx + */ + esp_err_t (*enter_sleep)(esp_lcd_touch_handle_t tp); + + /** + * @brief set touch controller into normal mode + * + * @note This function is usually blocking. + * + * @param tp: Touch handler + * + * @return + * - ESP_OK on success, otherwise returns ESP_ERR_xxx + */ + esp_err_t (*exit_sleep)(esp_lcd_touch_handle_t tp); + + /** + * @brief Read data from touch controller (mandatory) + * + * @note This function is usually blocking. + * + * @param tp: Touch handler + * + * @return + * - ESP_OK on success, otherwise returns ESP_ERR_xxx + */ + esp_err_t (*read_data)(esp_lcd_touch_handle_t tp); + + /** + * @brief Get coordinates from touch controller (mandatory) + * + * @param tp: Touch handler + * @param x: Array of X coordinates + * @param y: Array of Y coordinates + * @param strength: Array of strengths + * @param point_num: Count of points touched (equals with count of items in x and y array) + * @param max_point_num: Maximum count of touched points to return (equals with max size of x and y array) + * + * @return + * - Returns true, when touched and coordinates readed. Otherwise returns false. + */ + bool (*get_xy)(esp_lcd_touch_handle_t tp, uint16_t *x, uint16_t *y, uint16_t *strength, uint8_t *point_num, uint8_t max_point_num); + + +#if (CONFIG_ESP_LCD_TOUCH_MAX_BUTTONS > 0) + /** + * @brief Get button state (optional) + * + * @param tp: Touch handler + * @param n: Button index + * @param state: Button state + * + * @return + * - Returns true, when touched and coordinates readed. Otherwise returns false. + */ + esp_err_t (*get_button_state)(esp_lcd_touch_handle_t tp, uint8_t n, uint8_t *state); +#endif + + /** + * @brief Swap X and Y after read coordinates (optional) + * If set, then not used SW swapping. + * + * @param tp: Touch handler + * @param swap: Set swap value + * + * @return + * - ESP_OK on success, otherwise returns ESP_ERR_xxx + */ + esp_err_t (*set_swap_xy)(esp_lcd_touch_handle_t tp, bool swap); + + /** + * @brief Are X and Y coordinates swapped (optional) + * + * @param tp: Touch handler + * @param swap: Get swap value + * + * @return + * - ESP_OK on success, otherwise returns ESP_ERR_xxx + */ + esp_err_t (*get_swap_xy)(esp_lcd_touch_handle_t tp, bool *swap); + + /** + * @brief Mirror X after read coordinates + * If set, then not used SW mirroring. + * + * @param tp: Touch handler + * @param mirror: Set X mirror value + * + * @return + * - ESP_OK on success, otherwise returns ESP_ERR_xxx + */ + esp_err_t (*set_mirror_x)(esp_lcd_touch_handle_t tp, bool mirror); + + /** + * @brief Is mirrored X (optional) + * + * @param tp: Touch handler + * @param mirror: Get X mirror value + * + * @return + * - ESP_OK on success, otherwise returns ESP_ERR_xxx + */ + esp_err_t (*get_mirror_x)(esp_lcd_touch_handle_t tp, bool *mirror); + + /** + * @brief Mirror Y after read coordinates + * If set, then not used SW mirroring. + * + * @param tp: Touch handler + * @param mirror: Set Y mirror value + * + * @return + * - ESP_OK on success, otherwise returns ESP_ERR_xxx + */ + esp_err_t (*set_mirror_y)(esp_lcd_touch_handle_t tp, bool mirror); + + /** + * @brief Is mirrored Y (optional) + * + * @param tp: Touch handler + * @param mirror: Get Y mirror value + * + * @return + * - ESP_OK on success, otherwise returns ESP_ERR_xxx + */ + esp_err_t (*get_mirror_y)(esp_lcd_touch_handle_t tp, bool *mirror); + + /** + * @brief Delete Touch + * + * @param tp: Touch handler + * + * @return + * - ESP_OK on success, otherwise returns ESP_ERR_xxx + */ + esp_err_t (*del)(esp_lcd_touch_handle_t tp); + + /** + * @brief Configuration structure + */ + esp_lcd_touch_config_t config; + + /** + * @brief Communication interface + */ + esp_lcd_panel_io_handle_t io; + + /** + * @brief Data structure + */ + esp_lcd_touch_data_t data; +}; + +/** + * @brief Read data from touch controller + * + * @note This function is usually blocking. + * + * @param tp: Touch handler + * + * @return + * - ESP_OK on success + * - ESP_ERR_INVALID_ARG parameter error + * - ESP_FAIL sending command error, slave hasn't ACK the transfer + * - ESP_ERR_INVALID_STATE I2C driver not installed or not in master mode + * - ESP_ERR_TIMEOUT operation timeout because the bus is busy + */ +esp_err_t esp_lcd_touch_read_data(esp_lcd_touch_handle_t tp); + +/** + * @brief Read coordinates from touch controller + * + * @param tp: Touch handler + * @param x: Array of X coordinates + * @param y: Array of Y coordinates + * @param strength: Array of the strengths (can be NULL) + * @param point_num: Count of points touched (equals with count of items in x and y array) + * @param max_point_num: Maximum count of touched points to return (equals with max size of x and y array) + * + * @return + * - Returns true, when touched and coordinates readed. Otherwise returns false. + */ +bool esp_lcd_touch_get_coordinates(esp_lcd_touch_handle_t tp, uint16_t *x, uint16_t *y, uint16_t *strength, uint8_t *point_num, uint8_t max_point_num); + + +#if (CONFIG_ESP_LCD_TOUCH_MAX_BUTTONS > 0) +/** + * @brief Get button state + * + * @param tp: Touch handler + * @param n: Button index + * @param state: Button state + * + * @return + * - ESP_OK on success + * - ESP_ERR_NOT_SUPPORTED if this function is not supported by controller + * - ESP_ERR_INVALID_ARG if bad button index + */ +esp_err_t esp_lcd_touch_get_button_state(esp_lcd_touch_handle_t tp, uint8_t n, uint8_t *state); +#endif + +/** + * @brief Swap X and Y after read coordinates + * + * @param tp: Touch handler + * @param swap: Set swap value + * + * @return + * - ESP_OK on success + */ +esp_err_t esp_lcd_touch_set_swap_xy(esp_lcd_touch_handle_t tp, bool swap); + +/** + * @brief Are X and Y coordinates swapped + * + * @param tp: Touch handler + * @param swap: Get swap value + * + * @return + * - ESP_OK on success + */ +esp_err_t esp_lcd_touch_get_swap_xy(esp_lcd_touch_handle_t tp, bool *swap); + +/** + * @brief Mirror X after read coordinates + * + * @param tp: Touch handler + * @param mirror: Set X mirror value + * + * @return + * - ESP_OK on success + */ +esp_err_t esp_lcd_touch_set_mirror_x(esp_lcd_touch_handle_t tp, bool mirror); + +/** + * @brief Is mirrored X + * + * @param tp: Touch handler + * @param mirror: Get X mirror value + * + * @return + * - ESP_OK on success + */ +esp_err_t esp_lcd_touch_get_mirror_x(esp_lcd_touch_handle_t tp, bool *mirror); + +/** + * @brief Mirror Y after read coordinates + * + * @param tp: Touch handler + * @param mirror: Set Y mirror value + * + * @return + * - ESP_OK on success + */ +esp_err_t esp_lcd_touch_set_mirror_y(esp_lcd_touch_handle_t tp, bool mirror); + +/** + * @brief Is mirrored Y + * + * @param tp: Touch handler + * @param mirror: Get Y mirror value + * + * @return + * - ESP_OK on success + */ +esp_err_t esp_lcd_touch_get_mirror_y(esp_lcd_touch_handle_t tp, bool *mirror); + +/** + * @brief Delete touch (free all allocated memory and restart HW) + * + * @param tp: Touch handler + * + * @return + * - ESP_OK on success + */ +esp_err_t esp_lcd_touch_del(esp_lcd_touch_handle_t tp); + +/** + * @brief Register user callback called after the touch interrupt occurred + * + * @param tp: Touch handler + * @param callback: Interrupt callback + * + * @return + * - ESP_OK on success + */ +esp_err_t esp_lcd_touch_register_interrupt_callback(esp_lcd_touch_handle_t tp, esp_lcd_touch_interrupt_callback_t callback); + +/** + * @brief Register user callback called after the touch interrupt occurred with user data + * + * @param tp: Touch handler + * @param callback: Interrupt callback + * @param user_data: User data passed to callback + * + * @return + * - ESP_OK on success + */ +esp_err_t esp_lcd_touch_register_interrupt_callback_with_data(esp_lcd_touch_handle_t tp, esp_lcd_touch_interrupt_callback_t callback, void *user_data); + +/** + * @brief Enter sleep mode + * + * @param tp: Touch handler + * + * @return + * - ESP_OK on success + * - ESP_ERR_INVALID_ARG if parameter is invalid + */ +esp_err_t esp_lcd_touch_enter_sleep(esp_lcd_touch_handle_t tp); + +/** + * @brief Exit sleep mode + * + * @param tp: Touch handler + * + * @return + * - ESP_OK on success + * - ESP_ERR_INVALID_ARG if parameter is invalid + */ +esp_err_t esp_lcd_touch_exit_sleep(esp_lcd_touch_handle_t tp); + +#ifdef __cplusplus +} +#endif diff --git a/src/vendor/my_panel_io.c b/src/vendor/my_panel_io.c new file mode 100644 index 0000000..3c33516 --- /dev/null +++ b/src/vendor/my_panel_io.c @@ -0,0 +1,160 @@ +#ifdef BOARD_IS_JC3248W535_VENDOR +// Custom esp_lcd_panel_io_t implementation for QSPI on arduino-esp32 3.0.x. +// +// Replicates stock `esp_lcd_panel_io_spi` behavior for lcd_cmd_bits=32 + +// flags.quad_mode=true, which arduino-esp32 3.0.x doesn't ship. Behavior +// verified against ESP-IDF v5.2 src/esp_lcd_panel_io_spi.c: +// +// * Cmd is a SEPARATE 32-bit QIO data transaction. Not packed with params. +// Not split across cmd/addr phases. +// * Params (for tx_param) and color data (for tx_color) follow as additional +// data-phase-only QIO transactions. +// * CS is held LOW across all transactions of one logical operation via +// SPI_TRANS_CS_KEEP_ACTIVE — this requires the SPI peripheral to drive CS +// (spics_io_num must be set to the CS pin, NOT -1). +// * No explicit RAMWRCONT header on continuation chunks. The chip stays in +// write mode as long as CS stays asserted. +// +// Scope: display path only. + +#include +#include +#include "esp_log.h" +#include "esp_check.h" +#include "driver/spi_master.h" +#include "esp_lcd_panel_io.h" +#include "esp_lcd_panel_io_interface.h" +#include "esp_heap_caps.h" + +#include "my_panel_io.h" + +static const char *TAG = "my_panel_io"; + +#define SCRATCH_BYTES 4096 +#define HEADER_BYTES 4 + +typedef struct { + esp_lcd_panel_io_t base; + spi_device_handle_t spi; + uint8_t* scratch; // DMA-capable staging for PSRAM-sourced data + uint8_t cmdbuf[8]; // 32-bit packed cmd buffer (DMA-safe internal) +} my_io_t; + +static inline void pack_header(uint8_t* buf, int lcd_cmd) { + buf[0] = (lcd_cmd >> 24) & 0xFF; + buf[1] = (lcd_cmd >> 16) & 0xFF; + buf[2] = (lcd_cmd >> 8) & 0xFF; + buf[3] = (lcd_cmd ) & 0xFF; +} + +// Send the 32-bit packed lcd_cmd as its own QIO data-phase transaction. +// CS is kept active so subsequent transactions in the same op continue the +// transfer under one CS assertion. +static esp_err_t send_cmd(my_io_t* self, int lcd_cmd, bool more_to_follow) { + pack_header(self->cmdbuf, lcd_cmd); + spi_transaction_ext_t t = {}; + t.base.flags = SPI_TRANS_MODE_QIO + | SPI_TRANS_VARIABLE_CMD + | SPI_TRANS_VARIABLE_ADDR + | SPI_TRANS_VARIABLE_DUMMY + | (more_to_follow ? SPI_TRANS_CS_KEEP_ACTIVE : 0); + t.command_bits = 0; + t.address_bits = 0; + t.dummy_bits = 0; + t.base.tx_buffer = self->cmdbuf; + t.base.length = 32; // 4 bytes = 32 bits + return spi_device_polling_transmit(self->spi, (spi_transaction_t*)&t); +} + +// Send a data-phase-only QIO transaction. Caller controls CS via the +// cs_keep_active flag: true = keep asserted after this one, false = release. +// tx_buffer must be in DMA-capable memory. +static esp_err_t send_data(my_io_t* self, const uint8_t* buf, size_t nbytes, + bool cs_keep_active) { + spi_transaction_ext_t t = {}; + t.base.flags = SPI_TRANS_MODE_QIO + | SPI_TRANS_VARIABLE_CMD + | SPI_TRANS_VARIABLE_ADDR + | SPI_TRANS_VARIABLE_DUMMY + | (cs_keep_active ? SPI_TRANS_CS_KEEP_ACTIVE : 0); + t.command_bits = 0; + t.address_bits = 0; + t.dummy_bits = 0; + t.base.tx_buffer = buf; + t.base.length = nbytes * 8; + return spi_device_polling_transmit(self->spi, (spi_transaction_t*)&t); +} + +static esp_err_t my_tx_param(esp_lcd_panel_io_t* io, int lcd_cmd, + const void* param, size_t param_size) +{ + my_io_t* self = (my_io_t*)io; + bool has_param = (param && param_size > 0); + esp_err_t r = send_cmd(self, lcd_cmd, /*more_to_follow=*/has_param); + if (r != ESP_OK || !has_param) return r; + + // Copy param into DMA scratch (param is usually a stack-allocated + // compound literal from the vendor driver, not DMA-capable). + if (param_size > SCRATCH_BYTES) return ESP_ERR_INVALID_SIZE; + memcpy(self->scratch, param, param_size); + return send_data(self, self->scratch, param_size, /*cs_keep_active=*/false); +} + +static esp_err_t my_tx_color(esp_lcd_panel_io_t* io, int lcd_cmd, + const void* color, size_t color_size) +{ + my_io_t* self = (my_io_t*)io; + esp_err_t r = ESP_OK; + // Per the jc3248w535 skill: AXS15231B wants CS toggled per chunk with a + // RAMWR/RAMWRCONT header on each chunk. lcd_cmd from the vendor driver + // is already (0x32<<24)|(0x2C<<8) (or 0x3C for continuation). We emit + // that header on the first chunk and (0x32<<24)|(0x3C<<8) on all + // continuations. + const int cont_lcd_cmd = (0x32 << 24) | (0x3C << 8); + const uint8_t* src = (const uint8_t*)color; + size_t remaining = color_size; + bool first = true; + while (remaining) { + size_t data_capacity = SCRATCH_BYTES - HEADER_BYTES; + size_t chunk = remaining > data_capacity ? data_capacity : remaining; + + pack_header(self->scratch, first ? lcd_cmd : cont_lcd_cmd); + memcpy(self->scratch + HEADER_BYTES, src, chunk); + + // CS cycles per chunk (spics_io_num is set, peripheral does the toggle). + // No SPI_TRANS_CS_KEEP_ACTIVE. + r = send_data(self, self->scratch, HEADER_BYTES + chunk, + /*cs_keep_active=*/false); + if (r != ESP_OK) break; + first = false; + src += chunk; + remaining -= chunk; + } + return r; +} + +static esp_err_t my_del(esp_lcd_panel_io_t* io) { + my_io_t* self = (my_io_t*)io; + if (self->scratch) heap_caps_free(self->scratch); + free(self); + return ESP_OK; +} + +esp_err_t my_panel_io_new(spi_device_handle_t spi, int cs_pin, + esp_lcd_panel_io_handle_t *ret_io) +{ + (void)cs_pin; // CS is managed by the SPI peripheral via spics_io_num + if (!spi || !ret_io) return ESP_ERR_INVALID_ARG; + my_io_t* self = (my_io_t*)calloc(1, sizeof(my_io_t)); + if (!self) return ESP_ERR_NO_MEM; + self->spi = spi; + self->scratch = (uint8_t*)heap_caps_aligned_alloc(16, SCRATCH_BYTES, + MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL); + if (!self->scratch) { free(self); return ESP_ERR_NO_MEM; } + self->base.tx_param = my_tx_param; + self->base.tx_color = my_tx_color; + self->base.del = my_del; + *ret_io = &self->base; + return ESP_OK; +} +#endif // BOARD_IS_JC3248W535_VENDOR diff --git a/src/vendor/my_panel_io.h b/src/vendor/my_panel_io.h new file mode 100644 index 0000000..f57565a --- /dev/null +++ b/src/vendor/my_panel_io.h @@ -0,0 +1,25 @@ +#pragma once + +#include "esp_err.h" +#include "driver/spi_master.h" +#include "esp_lcd_panel_io.h" + +#ifdef __cplusplus +extern "C" { +#endif + +// Create a custom esp_lcd_panel_io_t that sends commands + params over QSPI +// using a single data-phase transaction (no cmd/addr phase split). +// +// The SPI device must already be added to the bus. The caller retains +// ownership of the spi handle; my_del does not free it. +// +// cs_pin is the GPIO pin for CS. This implementation drives CS manually +// around each transaction so the SPI device must have been configured with +// spics_io_num = -1. +esp_err_t my_panel_io_new(spi_device_handle_t spi, int cs_pin, + esp_lcd_panel_io_handle_t *ret_io); + +#ifdef __cplusplus +} +#endif From 20fb2044d4f8707bd9ddf442364635d7e0efd7db Mon Sep 17 00:00:00 2001 From: Niels Timmer Date: Wed, 22 Apr 2026 21:43:11 +0200 Subject: [PATCH 05/15] JC3248W535: add Arduino_GFX-based known-good skeleton diagnostic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the hand-rolled QSPI bus / panel code in src/skeleton_test.cpp with a minimal sketch built on moononournation/Arduino_GFX 1.5.x, which ships a production-grade Arduino_AXS15231B panel + Arduino_ESP32QSPI databus. Paints RED / GREEN / BLUE / WHITE / BLACK full-screen and halts. Key settings (discovered through this session): - Arduino_AXS15231B constructed with IPS=false. IPS=true sends INVON (0x21) during init, which on this panel revision ends up double-inverting — every color renders as its bitwise complement. IPS=false skips INVON and colors display correctly. - ESP32QSPI_MAX_PIXELS_AT_ONCE overridden to 320 (one row per chunk). The library default 1024 px/chunk produced a noise band at rows ~120-240 on this board; 320 px chunks eliminate it. - pclk dropped to 20 MHz for safety margin; skill recommends 32 MHz as the default start point but 20 is bulletproof on this board. Clean full-screen colors verified end-to-end on the physical board. --- platformio.ini | 6 + src/skeleton_test.cpp | 314 +++++------------------------------------- 2 files changed, 43 insertions(+), 277 deletions(-) diff --git a/platformio.ini b/platformio.ini index 639f432..a0d188b 100644 --- a/platformio.ini +++ b/platformio.ini @@ -183,11 +183,17 @@ build_flags = [env:jc3248w535_skel] extends = env:jc3248w535 build_src_filter = -<*> + +lib_deps = + ${common.lib_deps} + moononournation/GFX Library for Arduino@~1.5.0 build_flags = -D BOARD_VARIANT=\"jc3248w535_skel\" -D BOARD_IS_JC3248W535_SKEL=1 -D BOARD_HAS_PSRAM=1 -D ARDUINO_USB_CDC_ON_BOOT=1 + ; Force Arduino_GFX QSPI chunk size to one row (320 px) to test if the + ; 2nd-quarter noise band is chunk-boundary-related. + -D ESP32QSPI_MAX_PIXELS_AT_ONCE=320 ; ============================================================================= ; jc3248w535_vendor — baseline diagnostic. Compiles the MANUFACTURER'S diff --git a/src/skeleton_test.cpp b/src/skeleton_test.cpp index e380514..c76d39d 100644 --- a/src/skeleton_test.cpp +++ b/src/skeleton_test.cpp @@ -1,294 +1,54 @@ -// Bare-metal AXS15231B diagnostic — no LovyanGFX, no Panel_LCD, no abstraction. +// Clean-slate JC3248W535 diagnostic using the moononournation/Arduino_GFX +// library's built-in Arduino_AXS15231B panel class and Arduino_ESP32QSPI +// databus. Per jc3248w535 skill lines 623-640, this is "production-grade" +// on this chip and requires no driver code. // -// Goal: answer one question — does our QSPI framing produce correct pixel data? +// If this binary paints solid colors correctly, the hardware + toolchain are +// proven good and every previous symptom we chased was a bug in our driver. +// If it doesn't, something else is going on with this particular board. // -// The init sequence has been debugged to match the vendor byte-for-byte. If the -// init bytes + SPI mode 3 + pclk 40MHz + COLMOD 0x55 + RGB565 are all correct -// AND the panel is armed (DISPON sent), then a single writePixels call with a -// solid-color buffer should paint the screen that color. If it doesn't, the -// bug lives in the pixel-write code path (how we frame 0x32/0x002C00 + QIO data -// + CS management). If it does paint, the LovyanGFX integration was the bug. -// -// Touch-to-reset is kept so iteration loop remains fast. +// Pin map from skill (verified against vendor pincfg.h): +// CS=45, SCK=47, D0=21, D1=48, D2=40, D3=39, BL=1 #include -#include -#include -#include -#include -#include - -// --- Pins (JC3248W535, verified from vendor pincfg.h) --- -#define PIN_CS GPIO_NUM_45 -#define PIN_SCK GPIO_NUM_47 -#define PIN_D0 GPIO_NUM_21 -#define PIN_D1 GPIO_NUM_48 -#define PIN_D2 GPIO_NUM_40 -#define PIN_D3 GPIO_NUM_39 -#define PIN_BL 1 -#define PIN_SDA 4 -#define PIN_SCL 8 - -#define LCD_W 320 -#define LCD_H 480 -#define CHUNK_PIXELS 1024 - -static spi_device_handle_t g_spi = nullptr; -static uint8_t* g_dma = nullptr; // reused for pixel chunks - -// Send a command via the AXS "write-cmd" path (opcode 0x02, single-line). -// 4-byte header on the wire: 0x02, 0x00, cmd, 0x00, then optional data bytes. -static void cs_low() { gpio_set_level(PIN_CS, 0); } -static void cs_high() { gpio_set_level(PIN_CS, 1); } - -static void tx_cmd(uint8_t cmd, const uint8_t* data, size_t n) { - cs_low(); - spi_transaction_ext_t t = {}; - // Vendor BSP sends init commands over QIO (lcd_cmd_bits=32 + quad_mode=true). - // Adding MODE_QIO here makes our init header go 4-line like the vendor's — - // previous attempts without it went single-line and the chip's behaviour on - // receiving an init byte sequence timed at SIO vs QIO may be the bug. - t.base.flags = SPI_TRANS_MODE_QIO - | SPI_TRANS_MULTILINE_CMD - | SPI_TRANS_MULTILINE_ADDR; - t.base.cmd = 0x02; - t.base.addr = ((uint32_t)cmd) << 8; - if (n) { - t.base.tx_buffer = data; - t.base.length = n * 8; - } - spi_device_polling_transmit(g_spi, (spi_transaction_t*)&t); - cs_high(); -} - -static inline void tx_cmd_noarg(uint8_t cmd) { tx_cmd(cmd, nullptr, 0); } +#include -// Stream a chunk of pixels with the 0x32 RAMWR/RAMWRC header, QIO data. -// Caller is responsible for CS low at batch start and high at batch end. -static void tx_pixels_first(const uint8_t* buf_bswapped, size_t n_pixels, - bool use_ramwr) { - spi_transaction_ext_t t = {}; - t.base.flags = SPI_TRANS_MODE_QIO; - t.base.cmd = 0x32; - t.base.addr = use_ramwr ? 0x002C00 : 0x003C00; - t.base.tx_buffer = buf_bswapped; - t.base.length = n_pixels * 16; - spi_device_polling_transmit(g_spi, (spi_transaction_t*)&t); -} - -static void tx_pixels_cont(const uint8_t* buf_bswapped, size_t n_pixels) { - spi_transaction_ext_t t = {}; - t.base.flags = SPI_TRANS_MODE_QIO - | SPI_TRANS_VARIABLE_CMD - | SPI_TRANS_VARIABLE_ADDR - | SPI_TRANS_VARIABLE_DUMMY; - t.command_bits = 0; t.address_bits = 0; t.dummy_bits = 0; - t.base.tx_buffer = buf_bswapped; - t.base.length = n_pixels * 16; - spi_device_polling_transmit(g_spi, (spi_transaction_t*)&t); -} - -// --- Bus init --- -static bool bus_init() { - gpio_set_direction(PIN_CS, GPIO_MODE_OUTPUT); - gpio_set_level(PIN_CS, 1); - - spi_bus_config_t bcfg = {}; - bcfg.mosi_io_num = PIN_D0; - bcfg.miso_io_num = PIN_D1; - bcfg.sclk_io_num = PIN_SCK; - bcfg.quadwp_io_num = PIN_D2; - bcfg.quadhd_io_num = PIN_D3; - bcfg.max_transfer_sz = CHUNK_PIXELS * 2 + 16; - bcfg.flags = SPICOMMON_BUSFLAG_MASTER | SPICOMMON_BUSFLAG_GPIO_PINS; - esp_err_t r = spi_bus_initialize(SPI2_HOST, &bcfg, SPI_DMA_CH_AUTO); - Serial.printf("[bus] spi_bus_initialize -> %d\n", r); - if (r != ESP_OK) return false; - - spi_device_interface_config_t dcfg = {}; - dcfg.command_bits = 8; - dcfg.address_bits = 24; - dcfg.dummy_bits = 0; - dcfg.mode = 3; // vendor - dcfg.cs_ena_pretrans = 0; - dcfg.cs_ena_posttrans = 0; - dcfg.clock_speed_hz = 40 * 1000 * 1000; // vendor pclk - dcfg.spics_io_num = -1; - dcfg.queue_size = 1; - dcfg.flags = SPI_DEVICE_HALFDUPLEX; - r = spi_bus_add_device(SPI2_HOST, &dcfg, &g_spi); - Serial.printf("[bus] spi_bus_add_device -> %d (handle=%p)\n", r, g_spi); - if (r != ESP_OK) return false; - - g_dma = (uint8_t*)heap_caps_aligned_alloc(16, CHUNK_PIXELS * 2, MALLOC_CAP_DMA); - Serial.printf("[bus] dma_buf=%p\n", g_dma); - return g_dma != nullptr; -} - -// --- Vendor init table (from JC3248W535 skill: vendor_specific_init_default) --- -struct InitOp { uint8_t cmd; const uint8_t* data; uint16_t n; uint16_t delay_ms; }; - -static void apply_vendor_init_table() { - static const uint8_t d_BB1[] = {0x00,0x00,0x00,0x00,0x00,0x00,0x5A,0xA5}; - static const uint8_t d_A0[] = {0x00,0x10,0x00,0x02,0x00,0x00,0x64,0x3F,0x20,0x05,0x3F,0x3F,0x00,0x00,0x00,0x00,0x00}; - static const uint8_t d_A2[] = {0x30,0x04,0x0A,0x3C,0xEC,0x54,0xC4,0x30,0xAC,0x28,0x7F,0x7F,0x7F,0x20,0xF8,0x10,0x02,0xFF,0xFF,0xF0,0x90,0x01,0x32,0xA0,0x91,0xC0,0x20,0x7F,0xFF,0x00,0x54}; - static const uint8_t d_D0[] = {0x30,0xAC,0x21,0x24,0x08,0x09,0x10,0x01,0xAA,0x14,0xC2,0x00,0x22,0x22,0xAA,0x03,0x10,0x12,0x40,0x14,0x1E,0x51,0x15,0x00,0x40,0x10,0x00,0x03,0x3D,0x12}; - static const uint8_t d_A3[] = {0xA0,0x06,0xAA,0x08,0x08,0x02,0x0A,0x04,0x04,0x04,0x04,0x04,0x04,0x04,0x04,0x04,0x04,0x04,0x04,0x00,0x55,0x55}; - static const uint8_t d_C1[] = {0x33,0x04,0x02,0x02,0x71,0x05,0x24,0x55,0x02,0x00,0x41,0x00,0x53,0xFF,0xFF,0xFF,0x4F,0x52,0x00,0x4F,0x52,0x00,0x45,0x3B,0x0B,0x02,0x0D,0x00,0xFF,0x40}; - static const uint8_t d_C3[] = {0x00,0x00,0x00,0x50,0x03,0x00,0x00,0x00,0x01,0x80,0x01}; - static const uint8_t d_C4[] = {0x00,0x24,0x33,0x90,0x50,0xEA,0x64,0x32,0xC8,0x64,0xC8,0x32,0x90,0x90,0x11,0x06,0xDC,0xFA,0x04,0x03,0x80,0xFE,0x10,0x10,0x00,0x0A,0x0A,0x44,0x50}; - static const uint8_t d_C5[] = {0x18,0x00,0x00,0x03,0xFE,0x78,0x33,0x20,0x30,0x10,0x88,0xDE,0x0D,0x08,0x0F,0x0F,0x01,0x78,0x33,0x20,0x10,0x10,0x80}; - static const uint8_t d_C6[] = {0x05,0x0A,0x05,0x0A,0x00,0xE0,0x2E,0x0B,0x12,0x22,0x12,0x22,0x01,0x00,0x00,0x3F,0x6A,0x18,0xC8,0x22}; - static const uint8_t d_C7[] = {0x50,0x32,0x28,0x00,0xA2,0x80,0x8F,0x00,0x80,0xFF,0x07,0x11,0x9F,0x6F,0xFF,0x26,0x0C,0x0D,0x0E,0x0F}; - static const uint8_t d_C9[] = {0x33,0x44,0x44,0x01}; - static const uint8_t d_CF[] = {0x34,0x1E,0x88,0x58,0x13,0x18,0x56,0x18,0x1E,0x68,0xF7,0x00,0x65,0x0C,0x22,0xC4,0x0C,0x77,0x22,0x44,0xAA,0x55,0x04,0x04,0x12,0xA0,0x08}; - static const uint8_t d_D5[] = {0x3E,0x3E,0x88,0x00,0x44,0x04,0x78,0x33,0x20,0x78,0x33,0x20,0x04,0x28,0xD3,0x47,0x03,0x03,0x03,0x03,0x86,0x00,0x00,0x00,0x30,0x52,0x3F,0x40,0x40,0x96}; - static const uint8_t d_D6[] = {0x10,0x32,0x54,0x76,0x98,0xBA,0xDC,0xFE,0x95,0x00,0x01,0x83,0x75,0x36,0x20,0x75,0x36,0x20,0x3F,0x03,0x03,0x03,0x10,0x10,0x00,0x04,0x51,0x20,0x01,0x00}; - static const uint8_t d_D7[] = {0x0A,0x08,0x0E,0x0C,0x1E,0x18,0x19,0x1F,0x00,0x1F,0x1A,0x1F,0x3E,0x3E,0x04,0x00,0x1F,0x1F,0x1F}; - static const uint8_t d_D8[] = {0x0B,0x09,0x0F,0x0D,0x1E,0x18,0x19,0x1F,0x01,0x1F,0x1A,0x1F}; - static const uint8_t d_D9[] = {0x00,0x0D,0x0F,0x09,0x0B,0x1F,0x18,0x19,0x1F,0x01,0x1E,0x1A,0x1F}; - static const uint8_t d_DD[] = {0x0C,0x0E,0x08,0x0A,0x1F,0x18,0x19,0x1F,0x00,0x1E,0x1A,0x1F}; - static const uint8_t d_DF[] = {0x44,0x73,0x4B,0x69,0x00,0x0A,0x02,0x90}; - static const uint8_t d_E0[] = {0x19,0x20,0x0A,0x13,0x0E,0x09,0x12,0x28,0xD4,0x24,0x0C,0x35,0x13,0x31,0x36,0x2F,0x03}; - static const uint8_t d_E1[] = {0x38,0x20,0x09,0x12,0x0E,0x08,0x12,0x28,0xC5,0x24,0x0C,0x34,0x12,0x31,0x36,0x2F,0x27}; - static const uint8_t d_E2[] = {0x19,0x20,0x0A,0x11,0x09,0x06,0x11,0x25,0xD4,0x22,0x0B,0x33,0x12,0x2D,0x32,0x2F,0x03}; - static const uint8_t d_E3[] = {0x38,0x20,0x0A,0x11,0x09,0x06,0x11,0x25,0xC4,0x21,0x0A,0x32,0x11,0x2C,0x32,0x2F,0x27}; - static const uint8_t d_E4[] = {0x19,0x20,0x0D,0x14,0x0D,0x08,0x12,0x2A,0xD4,0x26,0x0E,0x35,0x13,0x34,0x39,0x2F,0x03}; - static const uint8_t d_E5[] = {0x38,0x20,0x0D,0x13,0x0D,0x07,0x12,0x29,0xC4,0x25,0x0D,0x35,0x12,0x33,0x39,0x2F,0x27}; - static const uint8_t d_BB2[] = {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}; - static const uint8_t d_2C[] = {0x00,0x00,0x00,0x00}; - - static const InitOp ops[] = { - {0xBB, d_BB1, sizeof(d_BB1), 0}, {0xA0, d_A0, sizeof(d_A0), 0}, - {0xA2, d_A2, sizeof(d_A2), 0}, {0xD0, d_D0, sizeof(d_D0), 0}, - {0xA3, d_A3, sizeof(d_A3), 0}, {0xC1, d_C1, sizeof(d_C1), 0}, - {0xC3, d_C3, sizeof(d_C3), 0}, {0xC4, d_C4, sizeof(d_C4), 0}, - {0xC5, d_C5, sizeof(d_C5), 0}, {0xC6, d_C6, sizeof(d_C6), 0}, - {0xC7, d_C7, sizeof(d_C7), 0}, {0xC9, d_C9, sizeof(d_C9), 0}, - {0xCF, d_CF, sizeof(d_CF), 0}, {0xD5, d_D5, sizeof(d_D5), 0}, - {0xD6, d_D6, sizeof(d_D6), 0}, {0xD7, d_D7, sizeof(d_D7), 0}, - {0xD8, d_D8, sizeof(d_D8), 0}, {0xD9, d_D9, sizeof(d_D9), 0}, - {0xDD, d_DD, sizeof(d_DD), 0}, {0xDF, d_DF, sizeof(d_DF), 0}, - {0xE0, d_E0, sizeof(d_E0), 0}, {0xE1, d_E1, sizeof(d_E1), 0}, - {0xE2, d_E2, sizeof(d_E2), 0}, {0xE3, d_E3, sizeof(d_E3), 0}, - {0xE4, d_E4, sizeof(d_E4), 0}, {0xE5, d_E5, sizeof(d_E5), 0}, - {0xBB, d_BB2, sizeof(d_BB2), 0}, - {0x13, nullptr, 0, 0}, // NORON - {0x11, nullptr, 0, 200}, // SLPOUT + 200ms - {0x29, nullptr, 0, 200}, // DISPON + 200ms - {0x2C, d_2C, sizeof(d_2C), 0}, // vendor trailing RAMWR param - }; - - spi_device_acquire_bus(g_spi, portMAX_DELAY); - for (auto& e : ops) { - tx_cmd(e.cmd, e.data, e.n); - if (e.delay_ms) delay(e.delay_ms); - } - spi_device_release_bus(g_spi); -} +static Arduino_DataBus *bus = new Arduino_ESP32QSPI( + 45 /*CS*/, 47 /*SCK*/, 21 /*D0*/, 48 /*D1*/, 40 /*D2*/, 39 /*D3*/); -static void panel_init() { - spi_device_acquire_bus(g_spi, portMAX_DELAY); - tx_cmd_noarg(0x01); // SWRESET — no HW reset pin on this board - delay(200); - uint8_t madctl = 0x00; - tx_cmd(0x36, &madctl, 1); - uint8_t colmod = 0x55; // RGB565 per AXS encoding - tx_cmd(0x3A, &colmod, 1); - spi_device_release_bus(g_spi); +// IPS=true sends INVON (0x21) during init, which double-inverts this panel +// (observed: every color appeared as its bitwise complement). Setting IPS=false +// skips INVON so colors display correctly. +static Arduino_GFX *gfx = new Arduino_AXS15231B( + bus, -1 /*RST*/, 0 /*rotation*/, false /*IPS*/, 320, 480); - apply_vendor_init_table(); // includes its own SLPOUT+DISPON -} - -// --- Draw: fill whole panel with one color --- -// CASET to full width, skip RASET (QSPI rule), then stream pixels until we've -// painted W*H. Pixel order in RAM is row-major from CASET top-left. -static void fill_screen(uint16_t color565) { - uint16_t sw = __builtin_bswap16(color565); - - spi_device_acquire_bus(g_spi, portMAX_DELAY); - - uint8_t col[4] = { 0, 0, (uint8_t)((LCD_W - 1) >> 8), (uint8_t)((LCD_W - 1) & 0xFF) }; - tx_cmd(0x2A, col, 4); - - // Preload DMA buffer with the swapped color (CHUNK_PIXELS at a time) - uint16_t* dst = (uint16_t*)g_dma; - for (size_t i = 0; i < CHUNK_PIXELS; ++i) dst[i] = sw; - - size_t total = (size_t)LCD_W * LCD_H; - bool first = true; - cs_low(); - while (total) { - size_t n = total > CHUNK_PIXELS ? CHUNK_PIXELS : total; - if (first) { tx_pixels_first(g_dma, n, /*use_ramwr=*/true); first = false; } - else { tx_pixels_cont(g_dma, n); } - total -= n; - } - cs_high(); - - spi_device_release_bus(g_spi); -} - -// --- Setup / loop --- void setup() { Serial.begin(115200); delay(300); - Serial.println("\n=== BARE-METAL AXS15231B DIAGNOSTIC ==="); - - pinMode(PIN_BL, OUTPUT); - digitalWrite(PIN_BL, HIGH); - Serial.println("[bl] backlight on"); - - Wire.begin(PIN_SDA, PIN_SCL); - Wire.setClock(400000); - - if (!bus_init()) { Serial.println("bus_init FAILED"); return; } - Serial.println("[bus] ready"); - - panel_init(); - Serial.println("[panel] init done"); - - Serial.println("fill_screen RED"); - fill_screen(0xF800); - delay(1500); - - Serial.println("fill_screen GREEN"); - fill_screen(0x07E0); - delay(1500); - - Serial.println("fill_screen BLUE"); - fill_screen(0x001F); - delay(1500); - - Serial.println("fill_screen WHITE"); - fill_screen(0xFFFF); - delay(1500); + Serial.println("\n=== AXS15231B Arduino_GFX baseline ==="); + + // Backlight on (LEDC not strictly needed; simple GPIO-high works for test) + pinMode(1, OUTPUT); + digitalWrite(1, HIGH); + + // Start at 20 MHz to rule out a SPI timing / DMA-boundary issue producing + // the corrupted band at rows ~120-240. Bump up to 32/40 MHz once we've + // confirmed a clean full-screen fill. + if (!gfx->begin(20000000UL)) { + Serial.println("gfx->begin() FAILED"); + return; + } + Serial.println("gfx begin OK"); - Serial.println("fill_screen BLACK"); - fill_screen(0x0000); + gfx->fillScreen(RED); Serial.println("RED"); delay(1500); + gfx->fillScreen(GREEN); Serial.println("GREEN"); delay(1500); + gfx->fillScreen(BLUE); Serial.println("BLUE"); delay(1500); + gfx->fillScreen(WHITE); Serial.println("WHITE"); delay(1500); + gfx->fillScreen(BLACK); Serial.println("BLACK"); delay(1500); Serial.println("Diagnostic halted"); } -static bool axs_touch_read() { - static const uint8_t read_cmd[11] = { - 0xB5, 0xAB, 0xA5, 0x5A, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00 - }; - Wire.beginTransmission(0x3B); - Wire.write(read_cmd, sizeof(read_cmd)); - if (Wire.endTransmission() != 0) return false; - uint8_t buf[8] = {0}; - size_t got = Wire.requestFrom((uint8_t)0x3B, (uint8_t)8); - if (got < 6) return false; - for (size_t i = 0; i < got && i < sizeof(buf); ++i) buf[i] = Wire.read(); - return (buf[0] == 0) && ((buf[1] & 0x0F) != 0); -} - void loop() { - if (axs_touch_read()) { - Serial.println("Touch detected — restarting"); - delay(50); - ESP.restart(); - } - delay(30); + delay(1000); } From 6fd42511040547000b7b13bd35b0c717c1044977 Mon Sep 17 00:00:00 2001 From: Niels Timmer Date: Wed, 22 Apr 2026 21:45:51 +0200 Subject: [PATCH 06/15] =?UTF-8?q?JC3248W535:=20drop=20skel=20safety=20marg?= =?UTF-8?q?ins=20=E2=80=94=20library=20defaults=20work?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ablation of the prior baseline's two safety-margin settings: - ESP32QSPI_MAX_PIXELS_AT_ONCE override removed — Arduino_GFX's default 1024 px/chunk paints cleanly; the noise band we chased with smaller chunks was actually caused by the touch auto-reset loop contaminating every observation, not by chunk-boundary issues. - gfx->begin() clock raised 20 MHz → 32 MHz (skill's recommended starting point). Full-screen fills remain clean. IPS=false (no INVON) remains — that one is real; the panel inverts if you let Arduino_AXS15231B send INVON. End result: skel env uses library defaults for everything except the well-documented IPS flag. Verified on physical board end-to-end. --- platformio.ini | 3 --- src/skeleton_test.cpp | 6 ++---- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/platformio.ini b/platformio.ini index a0d188b..c9d1e78 100644 --- a/platformio.ini +++ b/platformio.ini @@ -191,9 +191,6 @@ build_flags = -D BOARD_IS_JC3248W535_SKEL=1 -D BOARD_HAS_PSRAM=1 -D ARDUINO_USB_CDC_ON_BOOT=1 - ; Force Arduino_GFX QSPI chunk size to one row (320 px) to test if the - ; 2nd-quarter noise band is chunk-boundary-related. - -D ESP32QSPI_MAX_PIXELS_AT_ONCE=320 ; ============================================================================= ; jc3248w535_vendor — baseline diagnostic. Compiles the MANUFACTURER'S diff --git a/src/skeleton_test.cpp b/src/skeleton_test.cpp index c76d39d..799dc1a 100644 --- a/src/skeleton_test.cpp +++ b/src/skeleton_test.cpp @@ -31,10 +31,8 @@ void setup() { pinMode(1, OUTPUT); digitalWrite(1, HIGH); - // Start at 20 MHz to rule out a SPI timing / DMA-boundary issue producing - // the corrupted band at rows ~120-240. Bump up to 32/40 MHz once we've - // confirmed a clean full-screen fill. - if (!gfx->begin(20000000UL)) { + // 32 MHz pclk per skill's recommended starting point (skill line 42). + if (!gfx->begin(32000000UL)) { Serial.println("gfx->begin() FAILED"); return; } From 40cec15c4fa3ceb7ed2bcfc0e0d5d408239ac392 Mon Sep 17 00:00:00 2001 From: Niels Timmer Date: Wed, 22 Apr 2026 22:25:15 +0200 Subject: [PATCH 07/15] JC3248W535: switch production driver to Arduino_GFX-backed LovyanGFX wrapper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the broken hand-rolled LovyanGFX Panel_AXS15231B (488 lines) with a thin 180-line Panel_Device subclass that owns an Arduino_GFX (Arduino_ESP32QSPI + Arduino_AXS15231B) internally and forwards LovyanGFX's low-level drawing primitives (setWindow, writeBlock, writePixels, drawPixelPreclipped, writeFillRectPreclipped, writeImage) to it. Key points: - `tft` stays a `lgfx::LovyanGFX&` — no changes to the 275 drawing call sites across the codebase. BambuHelper's rendering code is unaware the underlying pixel transport changed. - `Arduino_AXS15231B` constructed with IPS=false (verified in skel env — IPS=true double-inverts colors on this panel revision). - pclk 32 MHz (Arduino_GFX handles its own bus + chunking). - Pin map hard-coded in the adapter since Arduino_GFX's databus takes pins at construction; the old AXS_QSPI_* build-flag pin defines were redundant and are removed. - Also cleans out the AXS_MINIMAL_TEST halt hook in main.cpp and the temporary pre-splash red-rectangle diagnostic in display_ui.cpp; the production boot path now runs unmodified on this board. - jc3248w535 env gets an explicit build_src_filter that excludes the skeleton_test / main_vendor diagnostics (they have their own setup/loop and would link-collide). Verified on device: full boot, display.init / setRotation / fillScreen all return cleanly, WiFi/AP/web server come up. No more custom QSPI bus. --- platformio.ini | 22 +- src/display_ui.cpp | 56 +--- src/lgfx_panel_axs15231b.hpp | 489 ------------------------------ src/lgfx_panel_axs15231b_agfx.hpp | 188 ++++++++++++ src/main.cpp | 8 - 5 files changed, 211 insertions(+), 552 deletions(-) delete mode 100644 src/lgfx_panel_axs15231b.hpp create mode 100644 src/lgfx_panel_axs15231b_agfx.hpp diff --git a/platformio.ini b/platformio.ini index c9d1e78..a673e4f 100644 --- a/platformio.ini +++ b/platformio.ini @@ -132,7 +132,8 @@ build_flags = ; ============================================================================= ; Guition JC3248W535 - 3.5" 320x480 IPS (AXS15231B QSPI + I2C touch) ; https://www.guition.com/ — ESP32-S3-N16R8, 16MB flash / 8MB PSRAM. -; Display driver is implemented locally (src/lgfx_panel_axs15231b.hpp) since +; Display driver wraps Arduino_GFX's Arduino_AXS15231B inside a LovyanGFX +; Panel_Device subclass (src/lgfx_panel_axs15231b_agfx.hpp), since ; mainline LovyanGFX does not ship Panel_AXS15231B. ; ; Touch RST/INT pins are not wired on the shipped Guition boards inspected — @@ -150,7 +151,12 @@ board_upload.flash_size = 16MB board_upload.maximum_size = 16777216 board_build.f_flash = 80000000L board_build.flash_mode = qio -lib_deps = ${common.lib_deps} +; Exclude the skeleton_test and vendor-shim diagnostics — those are used only +; by jc3248w535_skel / jc3248w535_vendor and they define their own setup/loop. +build_src_filter = +<*> - - - +lib_deps = + ${common.lib_deps} + moononournation/GFX Library for Arduino@~1.5.0 build_flags = -D BOARD_VARIANT=\"jc3248w535\" -D BOARD_IS_JC3248W535=1 @@ -158,22 +164,14 @@ build_flags = -D BOARD_HAS_PSRAM=1 -D ARDUINO_USB_CDC_ON_BOOT=1 ; --- Display (AXS15231B QSPI, 320x480 portrait native) --- + ; Pins are hard-coded inside Panel_AXS15231B_AGFX (src/lgfx_panel_axs15231b_agfx.hpp) + ; because Arduino_GFX's databus takes pins at construction, not at begin(). -D DISPLAY_320x480=1 - -D AXS_QSPI_HOST=1 ; SPI2_HOST - -D AXS_QSPI_FREQ=32000000 ; Arduino_GFX caps AXS15231B at 32 MHz - -D AXS_QSPI_CS=45 - -D AXS_QSPI_SCK=47 - -D AXS_QSPI_D0=21 - -D AXS_QSPI_D1=48 - -D AXS_QSPI_D2=40 - -D AXS_QSPI_D3=39 -D BACKLIGHT_PIN=1 ; --- Touch (AXS15231B touch controller on I2C @0x3B) --- -D USE_AXS_TOUCH=1 -D AXS_TOUCH_SDA=4 -D AXS_TOUCH_SCL=8 - ; TEMP: bypass WiFi/MQTT/splash — only run display init + one rect then halt. - -D AXS_MINIMAL_TEST=1 ; ============================================================================= ; jc3248w535_skel — standalone test binary using the axs15231b-lovyangfx diff --git a/src/display_ui.cpp b/src/display_ui.cpp index 3084637..81dfecc 100644 --- a/src/display_ui.cpp +++ b/src/display_ui.cpp @@ -205,38 +205,22 @@ static LGFX_WS154 _tft_instance; #elif defined(BOARD_IS_JC3248W535) // --- Guition JC3248W535 + AXS15231B 320x480 --------------------------------- -// Panel_AXS15231B is implemented locally (see lgfx_panel_axs15231b.hpp) and -// owns its own ESP-IDF spi_master bus in QSPI (4-line data) mode — mainline -// LovyanGFX has neither the driver nor a QSPI bus class. LGFX_Device's _bus -// pointer stays null: all pixel and command traffic goes through the panel's -// own transaction calls. CS is still driven by Panel_Device::cs_control() -// via cfg.pin_cs. -#include "lgfx_panel_axs15231b.hpp" +// Panel_AXS15231B_AGFX wraps moononournation/Arduino_GFX's Arduino_AXS15231B +// driver inside a LovyanGFX Panel_Device subclass. Mainline LovyanGFX has +// neither an AXS15231B panel class nor a QSPI bus, and a hand-rolled custom +// driver didn't produce correct pixels on this hardware. Arduino_GFX does — +// this wrapper lets the whole codebase keep calling the LovyanGFX API on +// `tft` while the physical QSPI traffic is handled by Arduino_GFX. +// Backlight is a simple GPIO-high (LEDC PWM not required for on/off). +#include "lgfx_panel_axs15231b_agfx.hpp" class LGFX_JC3248W535 : public lgfx::LGFX_Device { - lgfx::Panel_AXS15231B _panel; + lgfx::Panel_AXS15231B_AGFX _panel; public: LGFX_JC3248W535() { - _panel.setBusPins((spi_host_device_t)AXS_QSPI_HOST, - AXS_QSPI_SCK, - AXS_QSPI_D0, AXS_QSPI_D1, AXS_QSPI_D2, AXS_QSPI_D3, - AXS_QSPI_FREQ); - auto cfg = _panel.config(); - cfg.pin_cs = AXS_QSPI_CS; - cfg.pin_rst = -1; // no hardware reset wired; software reset in init - cfg.pin_busy = -1; - cfg.memory_width = 320; - cfg.memory_height = 480; - cfg.panel_width = 320; - cfg.panel_height = 480; - cfg.offset_x = 0; - cfg.offset_y = 0; - cfg.offset_rotation = 0; - cfg.readable = false; - cfg.invert = false; - cfg.rgb_order = false; // AXS15231B wants MADCTL bit3=1 for RGB - cfg.dlen_16bit = false; - cfg.bus_shared = false; - _panel.config(cfg); + // Panel_AXS15231B_AGFX owns the Arduino_GFX bus+panel internally. Pins + // are hard-coded in its constructor to the verified JC3248W535 map + // (CS=45, SCK=47, D0=21, D1=48, D2=40, D3=39) since Arduino_GFX's + // databus class hard-codes them at construction anyway. setPanel(&_panel); } }; @@ -439,16 +423,6 @@ void initDisplay() { memset(&prevState, 0, sizeof(prevState)); // Splash screen -#if defined(BOARD_IS_JC3248W535) - // Minimal-minimal test pattern. ONE 8-aligned red square away from the - // origin. If setWindow works, we see RED at (96, 200) size 32x24. If - // broken, we see red at (0,0) with extent 32x24 (or some variant). - Serial.println("Display: minimal test — RED 32x24 at (96, 200)"); - tft.fillRect(96, 200, 32, 24, 0xF800); -# if defined(AXS_MINIMAL_TEST) - return; // no splash, no drawString — main.cpp halts after we return -# endif -#endif tft.setTextDatum(MC_DATUM); tft.setTextColor(CLR_GREEN, CLR_BG); tft.setTextFont(4); @@ -458,10 +432,6 @@ void initDisplay() { tft.drawString("Printer Monitor", SCREEN_W / 2, SCREEN_H / 2 + 10); tft.setTextFont(1); tft.drawString(FW_VERSION, SCREEN_W / 2, SCREEN_H / 2 + 30); -#if defined(BOARD_IS_JC3248W535) - Serial.println("Display: splash hold"); - delay(8000); -#endif } void applyDisplaySettings() { diff --git a/src/lgfx_panel_axs15231b.hpp b/src/lgfx_panel_axs15231b.hpp deleted file mode 100644 index 791d419..0000000 --- a/src/lgfx_panel_axs15231b.hpp +++ /dev/null @@ -1,489 +0,0 @@ -// AXS15231B QSPI panel driver for LovyanGFX (ESP32-S3). -// -// Mainline LovyanGFX has no Panel_AXS15231B (issue #699). The chip requires -// real 4-line QSPI for pixel data — single-wire MOSI works for init but the -// controller switches to quad-mode input after the RAMWR/RAMWRCONT header -// and stops accepting data on D0 alone. Arduino_GFX's Arduino_ESP32QSPI -// uses ESP-IDF's spi_master with SPI_TRANS_MODE_QIO on the data path; we do -// the same here. This class inherits Panel_LCD so LovyanGFX's graphics -// layer (fillRect, drawString, smoothArc, sprites, VLW fonts) is unchanged. -// -// Protocol: -// Commands: cmd=0x02, addr={0x00,cmd,0x00} (24 bits), single-line on D0. -// Optional byte-data is sent single-line on D0 after the header. -// Pixels: cmd=0x32, addr={0x00,0x3C,0x00} (24 bits), QIO on D0..D3. -// (0x3C = RAMWRCONT, appended after the 0x2C RAMWR that -// setWindow() issues.) - -#pragma once - -#if defined(ESP_PLATFORM) - -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -namespace lgfx { -inline namespace v1 { - -struct Panel_AXS15231B : public Panel_LCD { - static constexpr uint32_t AXS_CHUNK_PIXELS = 1024; // matches Arduino_ESP32QSPI - static constexpr uint32_t AXS_CHUNK_BYTES = AXS_CHUNK_PIXELS * 2; - - static constexpr uint8_t CMD_SWRESET = 0x01; - static constexpr uint8_t CMD_SLPIN = 0x10; - static constexpr uint8_t CMD_SLPOUT = 0x11; - static constexpr uint8_t CMD_INVOFF = 0x20; - static constexpr uint8_t CMD_INVON = 0x21; - static constexpr uint8_t CMD_DISPOFF = 0x28; - static constexpr uint8_t CMD_DISPON = 0x29; - static constexpr uint8_t CMD_CASET = 0x2A; - static constexpr uint8_t CMD_RASET = 0x2B; - static constexpr uint8_t CMD_RAMWR = 0x2C; - static constexpr uint8_t CMD_MADCTL = 0x36; - static constexpr uint8_t CMD_COLMOD = 0x3A; - - // Init table — 320x480 type1, ported from moononournation/Arduino_GFX. - // Layout: cmd, arg_count, args... (sentinel 0xFF 0xFF ends the list). - static const uint8_t* init_ops() { - static const uint8_t ops[] = { - 0xA0, 17, - 0xC0, 0x10, 0x00, 0x02, 0x00, 0x00, 0x04, 0x3F, 0x20, 0x05, - 0x3F, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xA2, 31, - 0x30, 0x3C, 0x24, 0x14, 0xD0, 0x20, 0xFF, 0xE0, 0x40, 0x19, - 0x80, 0x80, 0x80, 0x20, 0xF9, 0x10, 0x02, 0xFF, 0xFF, 0xF0, - 0x90, 0x01, 0x32, 0xA0, 0x91, 0xE0, 0x20, 0x7F, 0xFF, 0x00, 0x5A, - 0xD0, 30, - 0xE0, 0x40, 0x51, 0x24, 0x08, 0x05, 0x10, 0x01, 0x20, 0x15, - 0xC2, 0x42, 0x22, 0x22, 0xAA, 0x03, 0x10, 0x12, 0x60, 0x14, - 0x1E, 0x51, 0x15, 0x00, 0x8A, 0x20, 0x00, 0x03, 0x3A, 0x12, - 0xA3, 22, - 0xA0, 0x06, 0xAA, 0x00, 0x08, 0x02, 0x0A, 0x04, 0x04, 0x04, - 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x00, 0x55, 0x55, - 0xC1, 30, - 0x31, 0x04, 0x02, 0x02, 0x71, 0x05, 0x24, 0x55, 0x02, 0x00, - 0x41, 0x00, 0x53, 0xFF, 0xFF, 0xFF, 0x4F, 0x52, 0x00, 0x4F, - 0x52, 0x00, 0x45, 0x3B, 0x0B, 0x02, 0x0D, 0x00, 0xFF, 0x40, - 0xC3, 11, - 0x00, 0x00, 0x00, 0x50, 0x03, 0x00, 0x00, 0x00, 0x01, 0x80, 0x01, - 0xC4, 29, - 0x00, 0x24, 0x33, 0x80, 0x00, 0xEA, 0x64, 0x32, 0xC8, 0x64, - 0xC8, 0x32, 0x90, 0x90, 0x11, 0x06, 0xDC, 0xFA, 0x00, 0x00, - 0x80, 0xFE, 0x10, 0x10, 0x00, 0x0A, 0x0A, 0x44, 0x50, - 0xC5, 23, - 0x18, 0x00, 0x00, 0x03, 0xFE, 0x3A, 0x4A, 0x20, 0x30, 0x10, - 0x88, 0xDE, 0x0D, 0x08, 0x0F, 0x0F, 0x01, 0x3A, 0x4A, 0x20, - 0x10, 0x10, 0x00, - 0xC6, 20, - 0x05, 0x0A, 0x05, 0x0A, 0x00, 0xE0, 0x2E, 0x0B, 0x12, 0x22, - 0x12, 0x22, 0x01, 0x03, 0x00, 0x3F, 0x6A, 0x18, 0xC8, 0x22, - 0xC7, 20, - 0x50, 0x32, 0x28, 0x00, 0xA2, 0x80, 0x8F, 0x00, 0x80, 0xFF, - 0x07, 0x11, 0x9C, 0x67, 0xFF, 0x24, 0x0C, 0x0D, 0x0E, 0x0F, - 0xC9, 4, 0x33, 0x44, 0x44, 0x01, - 0xCF, 27, - 0x2C, 0x1E, 0x88, 0x58, 0x13, 0x18, 0x56, 0x18, 0x1E, 0x68, - 0x88, 0x00, 0x65, 0x09, 0x22, 0xC4, 0x0C, 0x77, 0x22, 0x44, - 0xAA, 0x55, 0x08, 0x08, 0x12, 0xA0, 0x08, - 0xD5, 30, - 0x40, 0x8E, 0x8D, 0x01, 0x35, 0x04, 0x92, 0x74, 0x04, 0x92, - 0x74, 0x04, 0x08, 0x6A, 0x04, 0x46, 0x03, 0x03, 0x03, 0x03, - 0x82, 0x01, 0x03, 0x00, 0xE0, 0x51, 0xA1, 0x00, 0x00, 0x00, - 0xD6, 30, - 0x10, 0x32, 0x54, 0x76, 0x98, 0xBA, 0xDC, 0xFE, 0x93, 0x00, - 0x01, 0x83, 0x07, 0x07, 0x00, 0x07, 0x07, 0x00, 0x03, 0x03, - 0x03, 0x03, 0x03, 0x03, 0x00, 0x84, 0x00, 0x20, 0x01, 0x00, - 0xD7, 19, - 0x03, 0x01, 0x0B, 0x09, 0x0F, 0x0D, 0x1E, 0x1F, 0x18, 0x1D, - 0x1F, 0x19, 0x40, 0x8E, 0x04, 0x00, 0x20, 0xA0, 0x1F, - 0xD8, 12, - 0x02, 0x00, 0x0A, 0x08, 0x0E, 0x0C, 0x1E, 0x1F, 0x18, 0x1D, 0x1F, 0x19, - 0xD9, 12, - 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, - 0xDD, 12, - 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, - 0xDF, 8, 0x44, 0x73, 0x4B, 0x69, 0x00, 0x0A, 0x02, 0x90, - 0xE0, 17, - 0x3B, 0x28, 0x10, 0x16, 0x0C, 0x06, 0x11, 0x28, 0x5C, 0x21, - 0x0D, 0x35, 0x13, 0x2C, 0x33, 0x28, 0x0D, - 0xE1, 17, - 0x37, 0x28, 0x10, 0x16, 0x0B, 0x06, 0x11, 0x28, 0x5C, 0x21, - 0x0D, 0x35, 0x14, 0x2C, 0x33, 0x28, 0x0F, - 0xE2, 17, - 0x3B, 0x07, 0x12, 0x18, 0x0E, 0x0D, 0x17, 0x35, 0x44, 0x32, - 0x0C, 0x14, 0x14, 0x36, 0x3A, 0x2F, 0x0D, - 0xE3, 17, - 0x37, 0x07, 0x12, 0x18, 0x0E, 0x0D, 0x17, 0x35, 0x44, 0x32, - 0x0C, 0x14, 0x14, 0x36, 0x32, 0x2F, 0x0F, - 0xE4, 17, - 0x3B, 0x07, 0x12, 0x18, 0x0E, 0x0D, 0x17, 0x39, 0x44, 0x2E, - 0x0C, 0x14, 0x14, 0x36, 0x3A, 0x2F, 0x0D, - 0xE5, 17, - 0x37, 0x07, 0x12, 0x18, 0x0E, 0x0D, 0x17, 0x39, 0x44, 0x2E, - 0x0C, 0x14, 0x14, 0x36, 0x3A, 0x2F, 0x0F, - 0xA4, 16, - 0x85, 0x85, 0x95, 0x82, 0xAF, 0xAA, 0xAA, 0x80, 0x10, 0x30, - 0x40, 0x40, 0x20, 0xFF, 0x60, 0x30, - 0xA4, 4, 0x85, 0x85, 0x95, 0x85, - 0xBB, 8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xFF, 0xFF // end sentinel - }; - return ops; - } - - Panel_AXS15231B(void) { - _cfg.memory_width = _cfg.panel_width = 320; - _cfg.memory_height = _cfg.panel_height = 480; - } - - ~Panel_AXS15231B() { - if (_spi_dev) { spi_bus_remove_device(_spi_dev); _spi_dev = nullptr; } - if (_bus_attached) { spi_bus_free(_spi_host); _bus_attached = false; } - if (_dma_buf) { heap_caps_free(_dma_buf); _dma_buf = nullptr; } - } - - // Pin / clock setup — called before init(). The panel owns its own SPI - // bus; Panel_LCD's _bus pointer is ignored. - void setTrace(bool on) { _trace = on; } - - void setBusPins(spi_host_device_t host, int8_t sck, - int8_t d0, int8_t d1, int8_t d2, int8_t d3, - uint32_t freq_hz) { - _spi_host = host; - _pin_sck = sck; - _pin_d0 = d0; _pin_d1 = d1; _pin_d2 = d2; _pin_d3 = d3; - _freq = freq_hz; - } - - bool init(bool use_reset) override { - if (!Panel_Device::init(use_reset)) return false; - if (!ensure_bus()) return false; - - delay(100); - - // Absolute minimal init — match the skill's skeleton verbatim: - // SWRESET, DISPOFF, SLPIN, SLPOUT, DISPON, COLMOD=0x05. - // No MADCTL (use factory default orientation for this test). - send_cmd(CMD_SWRESET); delay(200); - send_cmd(CMD_DISPOFF); delay(20); - send_cmd(CMD_SLPIN); delay(20); - send_cmd(CMD_SLPOUT); delay(200); - send_cmd(CMD_DISPON); delay(20); - - uint8_t colmod = 0x05; - send_cmd(CMD_COLMOD, &colmod, 1); - delay(10); - _write_depth = rgb565_2Byte; - _write_bits = 16; - - return true; - } - - void beginTransaction(void) override { - if (_in_tx) return; - _in_tx = true; - if (_spi_dev) spi_device_acquire_bus(_spi_dev, portMAX_DELAY); - } - - void endTransaction(void) override { - if (!_in_tx) return; - _in_tx = false; - flush_pixels(); // safety net — close any open pixel batch - if (_spi_dev) spi_device_release_bus(_spi_dev); - } - - color_depth_t setColorDepth(color_depth_t depth) override { - uint8_t mode = 0; - if (depth == rgb565_2Byte) { mode = 0x05; _write_depth = depth; } - else if (depth == rgb666_3Byte) { mode = 0x06; _write_depth = depth; } - else return _write_depth; - send_cmd(CMD_COLMOD, &mode, 1); - return _write_depth; - } - - void setInvert(bool invert) override { - send_cmd(invert ? CMD_INVON : CMD_INVOFF); - } - - void setSleep(bool flg) override { - send_cmd(flg ? CMD_SLPIN : CMD_SLPOUT); - if (!flg) delay(150); - } - - void setPowerSave(bool /*flg*/) override {} - void waitDisplay(void) override {} - bool displayBusy(void) override { return false; } - - void setWindow(uint_fast16_t xs, uint_fast16_t ys, - uint_fast16_t xe, uint_fast16_t ye) override { - if ((xe - xs) >= _width) { xs = 0; xe = _width - 1; } - if ((ye - ys) >= _height) { ys = 0; ye = _height - 1; } - // Recovery gap after a QIO pixel batch — the panel sometimes refuses - // to accept a single-line CASET/RASET immediately after quad data. - esp_rom_delay_us(2); - uint8_t buf[4]; - buf[0] = xs >> 8; buf[1] = xs & 0xFF; buf[2] = xe >> 8; buf[3] = xe & 0xFF; - send_cmd(CMD_CASET, buf, 4); - esp_rom_delay_us(2); - buf[0] = ys >> 8; buf[1] = ys & 0xFF; buf[2] = ye >> 8; buf[3] = ye & 0xFF; - send_cmd(CMD_RASET, buf, 4); - esp_rom_delay_us(2); - _pixel_first = true; - Serial.printf("[AXS] setWindow (%u,%u)-(%u,%u)\n", - (unsigned)xs, (unsigned)ys, (unsigned)xe, (unsigned)ye); - } - - void drawPixelPreclipped(uint_fast16_t x, uint_fast16_t y, - uint32_t rawcolor) override { - setWindow(x, y, x, y); - // Single-pixel path — route through send_pixels_repeat_qio so the DMA - // buffer is used (stack pointers aren't DMA-capable on ESP32-S3). - send_pixels_repeat_qio((uint16_t)rawcolor, 1); - } - - void writeFillRectPreclipped(uint_fast16_t x, uint_fast16_t y, - uint_fast16_t w, uint_fast16_t h, - uint32_t rawcolor) override { - uint32_t len = (uint32_t)w * h; - setWindow(x, y, x + w - 1, y + h - 1); - send_pixels_repeat_qio((uint16_t)rawcolor, len); - } - - void writeBlock(uint32_t rawcolor, uint32_t len) override { - send_pixels_repeat_qio((uint16_t)rawcolor, len); - } - - void writePixels(pixelcopy_t* param, uint32_t len, bool /*use_dma*/) override { - // Convert into the DMA staging buffer in AXS_CHUNK_PIXELS-sized chunks - // and byte-swap for the panel (AXS15231B wants big-endian RGB565). - // send_pixels_qio_chunk consults _pixel_first so the first chunk after - // setWindow opens with RAMWR and everything after uses RAMWRCONT, even - // across multiple writePixels calls (e.g. writeImage looping rows). - uint8_t* staging = dma_buf(); - if (!staging) return; - while (len) { - uint32_t chunk = (len > AXS_CHUNK_PIXELS) ? AXS_CHUNK_PIXELS : len; - param->fp_copy(staging, 0, chunk, param); - uint16_t* dst = reinterpret_cast(staging); - for (uint32_t i = 0; i < chunk; ++i) { - uint16_t p = dst[i]; - dst[i] = (uint16_t)((p << 8) | (p >> 8)); - } - send_pixels_qio_chunk(staging, chunk * 2); - len -= chunk; - } - // writeImage calls writePixels per row — DON'T flush here or each row - // starts a fresh batch. writeImage handles flush at its end. - } - - void writeImage(uint_fast16_t x, uint_fast16_t y, - uint_fast16_t w, uint_fast16_t h, - pixelcopy_t* param, bool use_dma) override { - if (_trace) Serial.printf("[AXS] writeImage xy=(%u,%u) wh=(%u,%u)\n", - (unsigned)x, (unsigned)y, (unsigned)w, (unsigned)h); - setWindow(x, y, x + w - 1, y + h - 1); - auto sx = param->src_x; - do { - writePixels(param, w, use_dma); - param->src_x = sx; - param->src_y++; - } while (--h); - flush_pixels(); - } - - // Readback is not supported (write-only QSPI panel). - uint32_t readCommand(uint_fast16_t, uint_fast8_t, uint_fast8_t) override { return 0; } - uint32_t readData(uint_fast8_t, uint_fast8_t) override { return 0; } - void readRect(uint_fast16_t, uint_fast16_t, uint_fast16_t, uint_fast16_t, - void*, pixelcopy_t*) override {} - - protected: - spi_host_device_t _spi_host = SPI2_HOST; - spi_device_handle_t _spi_dev = nullptr; - bool _bus_attached = false; - bool _in_tx = false; - // True until the first pixel chunk after the most recent setWindow has - // been pushed. That chunk sends RAMWR (0x002C00) to anchor at the top - // of the window; every subsequent chunk sends RAMWRCONT (0x003C00) so - // the panel appends instead of re-anchoring. - bool _pixel_first = true; - // Debug — toggle at runtime to trace window/image/pixel calls. Cheap - // when off, noisy when on. Set by Panel_AXS15231B::setTrace(). - bool _trace = false; - int8_t _pin_sck = -1, _pin_d0 = -1, _pin_d1 = -1, _pin_d2 = -1, _pin_d3 = -1; - uint32_t _freq = 40000000; - uint8_t* _dma_buf = nullptr; - - uint8_t* dma_buf() { - if (!_dma_buf) { - _dma_buf = (uint8_t*)heap_caps_aligned_alloc( - 16, AXS_CHUNK_BYTES, MALLOC_CAP_DMA); - } - return _dma_buf; - } - - // Bypass Panel_Device::cs_control — go directly to GPIO to match the - // skeleton and Arduino_ESP32QSPI exactly. Panel_Device's default might - // do extra bookkeeping we don't need and that may interact badly with - // our manual pin management. - void cs_control(bool level) override { - if (_cfg.pin_cs >= 0) { - gpio_set_level((gpio_num_t)_cfg.pin_cs, level ? 1 : 0); - } - } - - bool ensure_bus() { - if (_spi_dev) return true; - if (_cfg.pin_cs >= 0) { - gpio_set_direction((gpio_num_t)_cfg.pin_cs, GPIO_MODE_OUTPUT); - gpio_set_level((gpio_num_t)_cfg.pin_cs, 1); - } - spi_bus_config_t bus = {}; - bus.mosi_io_num = _pin_d0; - bus.miso_io_num = _pin_d1; // ESP-IDF uses MISO pin slot for D1 - bus.sclk_io_num = _pin_sck; - bus.quadwp_io_num = _pin_d2; - bus.quadhd_io_num = _pin_d3; - bus.max_transfer_sz = AXS_CHUNK_BYTES + 16; - bus.flags = SPICOMMON_BUSFLAG_MASTER | SPICOMMON_BUSFLAG_GPIO_PINS; - esp_err_t err = spi_bus_initialize(_spi_host, &bus, SPI_DMA_CH_AUTO); - if (err != ESP_OK) { - ESP_LOGE("AXS15231B", "spi_bus_initialize failed: %d", err); - return false; - } - _bus_attached = true; - - spi_device_interface_config_t dev = {}; - dev.command_bits = 8; - dev.address_bits = 24; - dev.mode = 0; - dev.clock_speed_hz = _freq; - dev.spics_io_num = -1; // CS is handled by Panel_Device::cs_control - dev.queue_size = 1; - dev.flags = SPI_DEVICE_HALFDUPLEX; - err = spi_bus_add_device(_spi_host, &dev, &_spi_dev); - if (err != ESP_OK) { - ESP_LOGE("AXS15231B", "spi_bus_add_device failed: %d", err); - return false; - } - return dma_buf() != nullptr; - } - - void update_madctl(void) override { - uint8_t r = _internal_rotation; - // Match Panel_NV3041A's rgb_order convention: rgb_order=true -> RGB (0x00), - // rgb_order=false -> BGR (0x08). AXS15231B's MADCTL bit3 = BGR. - uint8_t rgb = _cfg.rgb_order ? 0x00 : 0x08; - uint8_t v; - switch (r) { - case 1: v = 0x40 | 0x20 | rgb; break; // MX|MV - case 2: v = 0x40 | 0x80 | rgb; break; // MX|MY - case 3: v = 0x80 | 0x20 | rgb; break; // MY|MV - default: v = rgb; break; - } - send_cmd(CMD_MADCTL, &v, 1); - if (_trace) Serial.printf("[AXS] MADCTL = 0x%02X (rot=%u rgb_order=%d)\n", - v, (unsigned)r, (int)_cfg.rgb_order); - } - - // Single-line command, optionally followed by single-line data bytes. - // Matches Arduino_ESP32QSPI exactly — small payloads (<=4 bytes) go via - // SPI_TRANS_USE_TXDATA (inline in the transaction struct, internal SPI - // FIFO — no DMA), which is how writeC8D16D16 handles CASET/RASET. Larger - // payloads use the DMA staging buffer. - void send_cmd(uint8_t cmd, const uint8_t* data = nullptr, size_t len = 0) { - if (!_spi_dev) return; - flush_pixels(); - cs_control(false); - spi_transaction_ext_t t = {}; - t.base.flags = SPI_TRANS_MULTILINE_CMD | SPI_TRANS_MULTILINE_ADDR; - t.base.cmd = 0x02; - t.base.addr = ((uint32_t)cmd) << 8; - if (len == 0) { - t.base.tx_buffer = nullptr; - t.base.length = 0; - } else if (len <= 4) { - t.base.flags |= SPI_TRANS_USE_TXDATA; - for (size_t i = 0; i < len; ++i) t.base.tx_data[i] = data[i]; - t.base.length = len * 8; - } else { - uint8_t* staging = dma_buf(); - if (!staging) { cs_control(true); return; } - size_t copy = (len > AXS_CHUNK_BYTES) ? AXS_CHUNK_BYTES : len; - memcpy(staging, data, copy); - t.base.tx_buffer = staging; - t.base.length = copy * 8; - } - spi_device_polling_transmit(_spi_dev, &t.base); - cs_control(true); - } - - // Send one chunk of pixel data in QIO mode. Matches the skill skeleton / - // LilyGo T-Display-S3 Long pattern: CS toggles per chunk, full header - // sent every time. First chunk uses RAMWR (0x002C00) to anchor at the - // setWindow origin; continuations use RAMWRCONT (0x003C00) to append. - // A single microsecond CS-high gap between chunks prevents the panel - // from interpreting the next header as appended pixel data. - void send_pixels_qio_chunk(const void* buf, size_t bytes) { - if (!_spi_dev || bytes == 0) return; - bool first = _pixel_first; - _pixel_first = false; - cs_control(false); - spi_transaction_ext_t t = {}; - t.base.flags = SPI_TRANS_MODE_QIO; - t.base.cmd = 0x32; - t.base.addr = first ? 0x002C00 : 0x003C00; - t.base.tx_buffer = buf; - t.base.length = bytes * 8; - esp_err_t err = spi_device_polling_transmit(_spi_dev, &t.base); - cs_control(true); - esp_rom_delay_us(1); - if (err != ESP_OK) { - ESP_LOGE("AXS15231B", "chunk bytes=%u err=%d", (unsigned)bytes, (int)err); - } - } - - // No-op now that CS toggles per chunk — kept so send_cmd / endTransaction - // callers don't need to know about it. Also resets _pixel_first so the - // next setWindow starts a fresh batch with a RAMWR header. - void flush_pixels() { _pixel_first = true; } - - - // Fill the DMA staging buffer with `color` (RGB565, panel byte order) and - // clock it out in chunks. `count` is pixel count. - void send_pixels_repeat_qio(uint16_t color, uint32_t count) { - uint8_t* staging = dma_buf(); - if (!staging) return; - uint16_t be = (uint16_t)((color << 8) | (color >> 8)); - uint32_t fill_pixels = (count < AXS_CHUNK_PIXELS) ? count : AXS_CHUNK_PIXELS; - uint16_t* dst = reinterpret_cast(staging); - for (uint32_t i = 0; i < fill_pixels; ++i) dst[i] = be; - - const uint32_t total = count; - uint32_t sent = 0; - while (count) { - uint32_t chunk = (count > AXS_CHUNK_PIXELS) ? AXS_CHUNK_PIXELS : count; - send_pixels_qio_chunk(staging, chunk * 2); - count -= chunk; - sent += chunk; - } - flush_pixels(); - (void)sent; (void)total; - } -}; - -} // namespace v1 -} // namespace lgfx - -#endif // ESP_PLATFORM diff --git a/src/lgfx_panel_axs15231b_agfx.hpp b/src/lgfx_panel_axs15231b_agfx.hpp new file mode 100644 index 0000000..eae0574 --- /dev/null +++ b/src/lgfx_panel_axs15231b_agfx.hpp @@ -0,0 +1,188 @@ +// LovyanGFX Panel_Device subclass that owns an internal Arduino_GFX instance +// (Arduino_ESP32QSPI + Arduino_AXS15231B) and forwards LovyanGFX's low-level +// drawing primitives to it. Lets the whole of BambuHelper keep calling +// `tft.fillRect()`, `tft.drawString()`, etc. on a LovyanGFX reference without +// changing any call sites, while the actual QSPI traffic is handled by the +// proven-working Arduino_GFX driver. +// +// Why this exists: mainline LovyanGFX has no AXS15231B panel class and our +// hand-rolled Panel_AXS15231B (src/lgfx_panel_axs15231b.hpp) never worked on +// this hardware. Arduino_GFX does work (see src/skeleton_test.cpp). This +// wrapper is "Option 3" from the jc3248w535 skill — wrap a proven external +// driver inside a LovyanGFX Panel subclass. + +#pragma once + +#include +#include + +namespace lgfx { +inline namespace v1 { + +class Panel_AXS15231B_AGFX : public Panel_Device { +public: + Panel_AXS15231B_AGFX() { + _cfg.memory_width = _cfg.panel_width = 320; + _cfg.memory_height = _cfg.panel_height = 480; + _cfg.offset_x = 0; + _cfg.offset_y = 0; + _cfg.offset_rotation = 0; + _cfg.dummy_read_pixel = 0; + _cfg.dummy_read_bits = 0; + _cfg.readable = false; + _cfg.invert = false; + _cfg.rgb_order = false; + _cfg.dlen_16bit = false; + _cfg.bus_shared = false; + _write_depth = color_depth_t::rgb565_2Byte; + _read_depth = color_depth_t::rgb565_2Byte; + } + + ~Panel_AXS15231B_AGFX() { + if (_agfx) { delete _agfx; _agfx = nullptr; } + if (_bus) { delete _bus; _bus = nullptr; } + } + + // ------------------------------------------------------------------------- + // Init / lifecycle + // ------------------------------------------------------------------------- + + bool init(bool /*use_reset*/) override { + if (_init_done) return true; + _bus = new Arduino_ESP32QSPI( + 45 /*CS*/, 47 /*SCK*/, 21 /*D0*/, 48 /*D1*/, 40 /*D2*/, 39 /*D3*/); + _agfx = new Arduino_AXS15231B( + _bus, + -1 /*RST, software-reset only*/, + 0 /*rotation*/, + false /*IPS — MUST be false to avoid double-inversion*/, + 320, 480); + if (!_agfx->begin(32000000UL)) { + delete _agfx; _agfx = nullptr; + delete _bus; _bus = nullptr; + return false; + } + _init_done = true; + _width = _cfg.panel_width; + _height = _cfg.panel_height; + return true; + } + + // Arduino_GFX owns its bus entirely, so LovyanGFX's bus plumbing is unused. + void initBus(void) override {} + void releaseBus(void) override {} + + void beginTransaction(void) override { + if (_agfx && !_in_transaction) { + _agfx->startWrite(); + _in_transaction = true; + } + } + + void endTransaction(void) override { + if (_agfx && _in_transaction) { + _agfx->endWrite(); + _in_transaction = false; + } + } + + color_depth_t setColorDepth(color_depth_t) override { + _write_depth = color_depth_t::rgb565_2Byte; + _read_depth = color_depth_t::rgb565_2Byte; + return _write_depth; + } + + void setRotation(uint_fast8_t r) override { + r &= 3; + _rotation = r; + _internal_rotation = r; + if (_agfx) _agfx->setRotation(r); + _width = (r & 1) ? _cfg.panel_height : _cfg.panel_width; + _height = (r & 1) ? _cfg.panel_width : _cfg.panel_height; + } + + void setInvert(bool /*invert*/) override {} + void setSleep(bool /*flg*/) override {} + void setPowerSave(bool /*flg*/) override {} + void waitDisplay(void) override {} + bool displayBusy(void) override { return false; } + + // ------------------------------------------------------------------------- + // Drawing primitives — LovyanGFX calls these after clipping. Everything + // higher-level (fillScreen, drawString, pushImage) funnels through here. + // ------------------------------------------------------------------------- + + void setWindow(uint_fast16_t xs, uint_fast16_t ys, + uint_fast16_t xe, uint_fast16_t ye) override { + if (!_agfx) return; + bool need_tx = !_in_transaction; + if (need_tx) _agfx->startWrite(); + _agfx->writeAddrWindow(xs, ys, (xe - xs + 1), (ye - ys + 1)); + if (need_tx) _agfx->endWrite(); + } + + void drawPixelPreclipped(uint_fast16_t x, uint_fast16_t y, + uint32_t rawcolor) override { + if (!_agfx) return; + _agfx->writePixel(x, y, (uint16_t)rawcolor); + } + + void writeFillRectPreclipped(uint_fast16_t x, uint_fast16_t y, + uint_fast16_t w, uint_fast16_t h, + uint32_t rawcolor) override { + if (!_agfx) return; + _agfx->writeFillRect(x, y, w, h, (uint16_t)rawcolor); + } + + void writeBlock(uint32_t rawcolor, uint32_t length) override { + if (!_agfx || length == 0) return; + _agfx->writeRepeat((uint16_t)rawcolor, length); + } + + void writePixels(pixelcopy_t* pc, uint32_t length, bool /*use_dma*/) override { + if (!_agfx || length == 0) return; + static constexpr uint32_t BUF_PIXELS = 128; + static uint16_t buf[BUF_PIXELS]; + while (length > 0) { + uint32_t n = length > BUF_PIXELS ? BUF_PIXELS : length; + pc->fp_copy(buf, 0, n, pc); + _agfx->writePixels(buf, n); + length -= n; + } + } + + void writeImage(uint_fast16_t x, uint_fast16_t y, + uint_fast16_t w, uint_fast16_t h, + pixelcopy_t* pc, bool use_dma) override { + if (!_agfx || w == 0 || h == 0) return; + bool need_tx = !_in_transaction; + if (need_tx) _agfx->startWrite(); + _agfx->writeAddrWindow(x, y, w, h); + writePixels(pc, (uint32_t)w * h, use_dma); + if (need_tx) _agfx->endWrite(); + } + + // ------------------------------------------------------------------------- + // Read path — the panel is not readable in QSPI mode. Return zeros. + // ------------------------------------------------------------------------- + + uint32_t readCommand(uint_fast16_t, uint_fast8_t, uint_fast8_t) override { return 0; } + uint32_t readData(uint_fast8_t, uint_fast8_t) override { return 0; } + void readRect(uint_fast16_t, uint_fast16_t, uint_fast16_t, uint_fast16_t, + void*, pixelcopy_t*) override {} + + int32_t getScanLine(void) override { return 0; } + + // Command/data ops are unused — we don't own a separate bus. + void writeCommand(uint32_t, uint_fast8_t) override {} + void writeData(uint32_t, uint_fast8_t) override {} + +private: + bool _init_done = false; + bool _in_transaction = false; + Arduino_DataBus* _bus = nullptr; + Arduino_TFT* _agfx = nullptr; // Arduino_TFT exposes writeAddrWindow/writeRepeat/writePixels +}; + +} // namespace v1 +} // namespace lgfx diff --git a/src/main.cpp b/src/main.cpp index 0598f15..6cc125f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -449,14 +449,6 @@ void setup() { loadSettings(); initDisplay(); -#if defined(BOARD_IS_JC3248W535) && defined(AXS_MINIMAL_TEST) - // MINIMAL-MINIMAL test: nothing after display init. No WiFi, no MQTT, - // no web server, no splash overlay. setBacklight() so we can actually - // see the panel, then halt. Flash over USB (COM12) to iterate. - Serial.println("AXS_MINIMAL_TEST: halting"); - setBacklight(128); - while (true) { delay(1000); } -#endif splashEnd = millis() + 2000; startWiFiDuringSplash(); setBacklight(brightness); From 7cc80476ee5f6f002fdd61714942c336f0f1b77b Mon Sep 17 00:00:00 2001 From: Niels Timmer Date: Wed, 22 Apr 2026 22:32:43 +0200 Subject: [PATCH 08/15] JC3248W535: guard skeleton_test.cpp behind BOARD_IS_JC3248W535_SKEL Without the guard, the skel diagnostic's setup()/loop() collided with BambuHelper main.cpp's when any non-skel env picked up src/skeleton_test.cpp via the default build_src_filter. Wrapping the whole file in `#ifdef BOARD_IS_JC3248W535_SKEL` turns it into an empty TU for every other env. All seven envs (esp32s3, cyd, ws_lcd_200, ws_lcd_154, esp32c3, jc3248w535, jc3248w535_skel, jc3248w535_vendor) now build clean. --- src/skeleton_test.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/skeleton_test.cpp b/src/skeleton_test.cpp index 799dc1a..d7f3f97 100644 --- a/src/skeleton_test.cpp +++ b/src/skeleton_test.cpp @@ -9,6 +9,9 @@ // // Pin map from skill (verified against vendor pincfg.h): // CS=45, SCK=47, D0=21, D1=48, D2=40, D3=39, BL=1 +// +// Guarded so only the jc3248w535_skel env compiles this translation unit. +#ifdef BOARD_IS_JC3248W535_SKEL #include #include @@ -50,3 +53,5 @@ void setup() { void loop() { delay(1000); } + +#endif // BOARD_IS_JC3248W535_SKEL From 1824295572314e5d080b3917a113b57f744f1574 Mon Sep 17 00:00:00 2001 From: Niels Timmer Date: Thu, 23 Apr 2026 11:44:21 +0200 Subject: [PATCH 09/15] JC3248W535: sprite-based push to bypass AXS15231B QSPI addressing limits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The AXS15231B in QSPI mode can't address arbitrary Y — every RAMWR resets the internal y-pointer to 0 within the CASET column window, so per-call draws at non-zero y always land at the top of the screen. Arduino_GFX also unconditionally sends RASET after CASET which maps every sub-width draw to the origin corner regardless of x/y. And with LovyanGFX's default chunked pushSprite path, the multiple RAMWRC continuations across separate CS cycles scramble the image. Fix: treat the panel as a framebuffer sink only. Draw the whole UI into a 320x480 PSRAM sprite, then flush via a new pushRawPixels escape-hatch that emits exactly one Arduino_GFX writePixels call — single CS cycle, one RAMWRC header, 150 internal VARIABLE-CMD continuations with CS held LOW. Also subclass Arduino_AXS15231B to skip RASET and explicitly set COLMOD=0x05 after init (Arduino_GFX's init table omits it, relying on POR defaults that differ across batches). Shapes, sizes and positions all render correctly with this baseline. Colors are still rotated (RED<->BLUE<->GREEN cyclic, YELLOW->MAGENTA) consistent with byte-swapped RGB565 reception — remaining follow-up. Diagnostic sprite test gated behind DIAG_LGFX_POST_INIT build flag so this commit can serve as a reproducible known-good state. --- platformio.ini | 4 + src/display_ui.cpp | 10 ++- src/display_ui.h | 8 ++ src/lgfx_panel_axs15231b_agfx.hpp | 137 ++++++++++++++++++++++++++---- src/main.cpp | 49 +++++++++++ 5 files changed, 189 insertions(+), 19 deletions(-) diff --git a/platformio.ini b/platformio.ini index a673e4f..3320e41 100644 --- a/platformio.ini +++ b/platformio.ini @@ -172,6 +172,10 @@ build_flags = -D USE_AXS_TOUCH=1 -D AXS_TOUCH_SDA=4 -D AXS_TOUCH_SCL=8 + ; --- DIAG re-test: after init cascade + initDisplay's fillScreen(CLR_BG), + ; run 5 tft.fillScreen()s and halt. Clean test of LGFX path with wrapper + ; fix applied (no sanity fillScreens in init()). + -D DIAG_LGFX_POST_INIT=1 ; ============================================================================= ; jc3248w535_skel — standalone test binary using the axs15231b-lovyangfx diff --git a/src/display_ui.cpp b/src/display_ui.cpp index 81dfecc..52a0f30 100644 --- a/src/display_ui.cpp +++ b/src/display_ui.cpp @@ -223,9 +223,9 @@ class LGFX_JC3248W535 : public lgfx::LGFX_Device { // databus class hard-codes them at construction anyway. setPanel(&_panel); } + lgfx::Panel_AXS15231B_AGFX* panelAXS() { return &_panel; } }; static LGFX_JC3248W535 _tft_instance; - #elif defined(BOARD_IS_C3) // --- ESP32-C3 Super Mini + ST7789 240x280 ------------------------------------ class LGFX_C3 : public lgfx::LGFX_Device { @@ -277,6 +277,14 @@ static LGFX_C3 _tft_instance; lgfx::LovyanGFX* tft_ptr = &_tft_instance; lgfx::LovyanGFX& tft = *tft_ptr; +// Direct panel pointer for JC3248W535 sprite escape-hatch; nullptr on all +// other boards so the extern declaration in display_ui.h is always satisfied. +#if defined(BOARD_IS_JC3248W535) +lgfx::Panel_AXS15231B_AGFX* g_axs_panel = _tft_instance.panelAXS(); +#else +lgfx::Panel_AXS15231B_AGFX* g_axs_panel = nullptr; +#endif + // Use user-configured bg color instead of hardcoded CLR_BG #undef CLR_BG #define CLR_BG (dispSettings.bgColor) diff --git a/src/display_ui.h b/src/display_ui.h index 5df51d2..8af7cc2 100644 --- a/src/display_ui.h +++ b/src/display_ui.h @@ -3,6 +3,10 @@ #include +// Forward-declare the panel type so callers can use the pointer without +// pulling in the full header (which includes Arduino_GFX headers). +namespace lgfx { inline namespace v1 { class Panel_AXS15231B_AGFX; } } + enum ScreenState { SCREEN_SPLASH, SCREEN_AP_MODE, @@ -21,6 +25,10 @@ extern lgfx::LovyanGFX* tft_ptr; // Convenience reference — all callers use `tft.method()` unchanged. extern lgfx::LovyanGFX& tft; +// Direct pointer to the AXS15231B panel wrapper; only non-null on +// BOARD_IS_JC3248W535 builds. Used by the sprite direct-push diagnostic. +extern lgfx::Panel_AXS15231B_AGFX* g_axs_panel; + void initDisplay(); void updateDisplay(); void setScreenState(ScreenState state); diff --git a/src/lgfx_panel_axs15231b_agfx.hpp b/src/lgfx_panel_axs15231b_agfx.hpp index eae0574..f60668e 100644 --- a/src/lgfx_panel_axs15231b_agfx.hpp +++ b/src/lgfx_panel_axs15231b_agfx.hpp @@ -16,6 +16,55 @@ #include #include +// --------------------------------------------------------------------------- +// Arduino_AXS15231B_QSPI — subclass of Arduino_AXS15231B that fixes two +// QSPI-mode quirks the stock driver gets wrong (both documented in the +// jc3248w535 skill's failure-mode table): +// +// 1. RASET (0x2B) must NOT be sent in QSPI mode. The panel derives rows +// from the RAMWR/RAMWRC pixel stream itself. Sending RASET after CASET +// causes "Only one corner displays, rest is garbage" — every draw lands +// at the chip's RAM origin regardless of the requested (x,y). Confirmed +// against the manufacturer's esp_lcd_axs15231b.c which only sends +// RASET when `flags.use_qspi_interface == 0`. +// +// 2. COLMOD (0x3A) is not set by Arduino_GFX's init table. On some batches +// the POR default is RGB666 (3-byte pixels), which misaligns against +// our 16-bit RGB565 DMA output and produces a stripe/rainbow pattern. +// Send 0x05 (RGB565, 16bpp) once after begin() to lock the format. +// --------------------------------------------------------------------------- +class Arduino_AXS15231B_QSPI : public Arduino_AXS15231B { +public: + using Arduino_AXS15231B::Arduino_AXS15231B; + + bool begin(int32_t speed = GFX_NOT_DEFINED) override { + if (!Arduino_AXS15231B::begin(speed)) return false; + // Force RGB565 / 16bpp COLMOD. Done after the base init sequence so it + // overrides any POR default and any drift during init. + _bus->beginWrite(); + _bus->writeC8D8(0x3A /*COLMOD*/, 0x05 /*RGB565 16bpp, AXS15231B encoding*/); + _bus->endWrite(); + return true; + } + + void writeAddrWindow(int16_t x, int16_t y, uint16_t w, + uint16_t h) override { + if ((x != _currentX) || (w != _currentW)) { + _currentX = x; + _currentW = w; + x += _xStart; + _bus->writeC8D16D16(AXS15231B_CASET, x, x + w - 1); + } + // RASET intentionally skipped — QSPI mode derives y from pixel stream. + // Cache y/h for consistency with parent expectations, but never send + // 0x2B. The panel uses RAMWR (0x2C) in writeCommand below to reset the + // write pointer to the top of the CASET window. + _currentY = y; + _currentH = h; + _bus->writeCommand(AXS15231B_RAMWR); + } +}; + namespace lgfx { inline namespace v1 { @@ -39,8 +88,8 @@ class Panel_AXS15231B_AGFX : public Panel_Device { } ~Panel_AXS15231B_AGFX() { - if (_agfx) { delete _agfx; _agfx = nullptr; } - if (_bus) { delete _bus; _bus = nullptr; } + if (_agfx) { delete _agfx; _agfx = nullptr; } + if (_agfx_bus) { delete _agfx_bus; _agfx_bus = nullptr; } } // ------------------------------------------------------------------------- @@ -49,19 +98,29 @@ class Panel_AXS15231B_AGFX : public Panel_Device { bool init(bool /*use_reset*/) override { if (_init_done) return true; - _bus = new Arduino_ESP32QSPI( + _agfx_bus = new Arduino_ESP32QSPI( 45 /*CS*/, 47 /*SCK*/, 21 /*D0*/, 48 /*D1*/, 40 /*D2*/, 39 /*D3*/); - _agfx = new Arduino_AXS15231B( - _bus, + _agfx = new Arduino_AXS15231B_QSPI( + _agfx_bus, -1 /*RST, software-reset only*/, 0 /*rotation*/, false /*IPS — MUST be false to avoid double-inversion*/, 320, 480); if (!_agfx->begin(32000000UL)) { - delete _agfx; _agfx = nullptr; - delete _bus; _bus = nullptr; + delete _agfx; _agfx = nullptr; + delete _agfx_bus; _agfx_bus = nullptr; return false; } + // IMPORTANT: do NOT call _agfx->fillScreen() here. Arduino_GFX's begin() + // ends with setAddrWindow(0,0,w,h) which caches _currentX/Y/W/H. A + // fillScreen at this point would skip the CASET/RASET re-send because + // state already matches — and the AXS15231B appears to need those + // explicitly re-sent after the long init sequence, or only a small sliver + // at the chip's RAM origin paints. LovyanGFX's post-init setRotation(0) + // call invalidates the Arduino_TFT _current* cache via Arduino_TFT:: + // setRotation (sets to 0xFFFF), which forces the next writeAddrWindow to + // re-send CASET and RASET. After that, fills work correctly. See the + // Arduino_TFT.cpp:137-177 for the cache logic. _init_done = true; _width = _cfg.panel_width; _height = _cfg.panel_height; @@ -107,6 +166,15 @@ class Panel_AXS15231B_AGFX : public Panel_Device { void waitDisplay(void) override {} bool displayBusy(void) override { return false; } + // Panel_Device's defaults for these four all dereference `_bus` (inherited + // from IPanel, nullptr in this wrapper because Arduino_GFX owns its own + // bus). Override to no-ops — Arduino_GFX flushes each draw immediately so + // there's no deferred DMA state to wait on. + void initDMA(void) override {} + void waitDMA(void) override {} + bool dmaBusy(void) override { return false; } + void display(uint_fast16_t, uint_fast16_t, uint_fast16_t, uint_fast16_t) override {} + // ------------------------------------------------------------------------- // Drawing primitives — LovyanGFX calls these after clipping. Everything // higher-level (fillScreen, drawString, pushImage) funnels through here. @@ -115,23 +183,27 @@ class Panel_AXS15231B_AGFX : public Panel_Device { void setWindow(uint_fast16_t xs, uint_fast16_t ys, uint_fast16_t xe, uint_fast16_t ye) override { if (!_agfx) return; - bool need_tx = !_in_transaction; - if (need_tx) _agfx->startWrite(); + // writeAddrWindow is transaction-internal (uses the bus mid-write). + // LovyanGFX always wraps setWindow in beginTransaction/endTransaction, + // so the bus is already open when we get here. _agfx->writeAddrWindow(xs, ys, (xe - xs + 1), (ye - ys + 1)); - if (need_tx) _agfx->endWrite(); } void drawPixelPreclipped(uint_fast16_t x, uint_fast16_t y, uint32_t rawcolor) override { if (!_agfx) return; - _agfx->writePixel(x, y, (uint16_t)rawcolor); + // Arduino_GFX's write*Preclipped variants assume we're inside an open + // startWrite/endWrite transaction — which we always are when LovyanGFX + // calls these, because LGFXBase wraps draws in beginTransaction which + // we map to _agfx->startWrite(). + _agfx->writePixelPreclipped(x, y, (uint16_t)rawcolor); } void writeFillRectPreclipped(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h, uint32_t rawcolor) override { if (!_agfx) return; - _agfx->writeFillRect(x, y, w, h, (uint16_t)rawcolor); + _agfx->writeFillRectPreclipped(x, y, w, h, (uint16_t)rawcolor); } void writeBlock(uint32_t rawcolor, uint32_t length) override { @@ -141,7 +213,11 @@ class Panel_AXS15231B_AGFX : public Panel_Device { void writePixels(pixelcopy_t* pc, uint32_t length, bool /*use_dma*/) override { if (!_agfx || length == 0) return; - static constexpr uint32_t BUF_PIXELS = 128; + // Use a large staging buffer so a full-screen pushSprite becomes a + // single Arduino_GFX writePixels call (one CS cycle, one RAMWRC header + // followed by VARIABLE-CMD continuation chunks with CS held low). + // 4096 pixels × 2 bytes = 8 KB static buf; no stack impact. + static constexpr uint32_t BUF_PIXELS = 4096; static uint16_t buf[BUF_PIXELS]; while (length > 0) { uint32_t n = length > BUF_PIXELS ? BUF_PIXELS : length; @@ -155,11 +231,26 @@ class Panel_AXS15231B_AGFX : public Panel_Device { uint_fast16_t w, uint_fast16_t h, pixelcopy_t* pc, bool use_dma) override { if (!_agfx || w == 0 || h == 0) return; - bool need_tx = !_in_transaction; - if (need_tx) _agfx->startWrite(); + // Always inside an open transaction when LovyanGFX calls us. _agfx->writeAddrWindow(x, y, w, h); writePixels(pc, (uint32_t)w * h, use_dma); - if (need_tx) _agfx->endWrite(); + } + + // ------------------------------------------------------------------------- + // Direct push escape-hatch: call _agfx->writePixels() with the whole + // contiguous pixel buffer in ONE call. Arduino_ESP32QSPI::writePixels + // then handles it as one CS cycle with a single RAMWRC header followed + // by internal VARIABLE-flag continuation chunks. This is the only way + // to push a large framebuffer without the chip getting confused by + // multiple RAMWRC commands from chunked calls. Intended for full-frame + // sprite flushes (e.g. pushing a PSRAM LGFX_Sprite as one atomic frame). + // ------------------------------------------------------------------------- + void pushRawPixels(uint16_t* data, uint32_t length) { + if (!_agfx || length == 0) return; + _agfx->startWrite(); + _agfx->writeAddrWindow(0, 0, _cfg.panel_width, _cfg.panel_height); + _agfx->writePixels(data, length); + _agfx->endWrite(); } // ------------------------------------------------------------------------- @@ -177,11 +268,21 @@ class Panel_AXS15231B_AGFX : public Panel_Device { void writeCommand(uint32_t, uint_fast8_t) override {} void writeData(uint32_t, uint_fast8_t) override {} + // Alpha-blend / copy paths — not exercised by BambuHelper, and the + // Panel_Device defaults touch `_bus`. Stub them to no-ops. + void writeImageARGB(uint_fast16_t, uint_fast16_t, + uint_fast16_t, uint_fast16_t, + pixelcopy_t*) override {} + void copyRect(uint_fast16_t, uint_fast16_t, + uint_fast16_t, uint_fast16_t, + uint_fast16_t, uint_fast16_t) override {} + private: bool _init_done = false; bool _in_transaction = false; - Arduino_DataBus* _bus = nullptr; - Arduino_TFT* _agfx = nullptr; // Arduino_TFT exposes writeAddrWindow/writeRepeat/writePixels + // Renamed from `_bus` to avoid shadowing Panel_Device's protected IBus*_bus. + Arduino_DataBus* _agfx_bus = nullptr; + Arduino_TFT* _agfx = nullptr; // exposes writeAddrWindow/writeRepeat/writePixels }; } // namespace v1 diff --git a/src/main.cpp b/src/main.cpp index 6cc125f..82a5481 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,4 +1,9 @@ #include +// Full panel definition needed for pushRawPixels escape-hatch (sprite diag). +// Must precede display_ui.h so the forward-declaration there is superseded. +#if defined(BOARD_IS_JC3248W535) +#include "lgfx_panel_axs15231b_agfx.hpp" +#endif #include "display_ui.h" #include "settings.h" #include "wifi_manager.h" @@ -449,6 +454,50 @@ void setup() { loadSettings(); initDisplay(); +#if defined(BOARD_IS_JC3248W535) && defined(DIAG_LGFX_POST_INIT) + // ---- SPRITE-BASED DIAGNOSTIC --------------------------------------------- + // The AXS15231B in QSPI mode cannot address arbitrary Y — every RAMWR + // resets the internal y-pointer to 0 within the CASET column window. + // Small draws at (x, y != 0) therefore always land at top-of-screen. + // Workaround: draw everything into an off-screen sprite in PSRAM, then + // push the whole sprite to the panel in ONE contiguous raster write. + // pushSprite issues a single setWindow(0,0,319,479) + bulk pixel write, + // which the chip handles correctly (all pixels are contiguous y from 0). + Serial.println("[diag-post] allocating 320x480 sprite in PSRAM"); + static lgfx::LGFX_Sprite sprite(&tft); + sprite.setPsram(true); + sprite.setColorDepth(16); + if (!sprite.createSprite(320, 480)) { + Serial.println("[diag-post] sprite alloc FAILED — halting"); + while (true) { delay(1000); } + } + Serial.printf("[diag-post] sprite alloc OK, free PSRAM=%u\n", + (unsigned)heap_caps_get_free_size(MALLOC_CAP_SPIRAM)); + // Draw the same test pattern into the sprite + sprite.fillScreen(0x0000); + sprite.fillRect(50, 50, 200, 100, 0xF800); // RED + sprite.fillRect(20, 200, 280, 40, 0x07E0); // GREEN + sprite.fillRect(0, 300, 320, 20, 0x001F); // BLUE + sprite.drawFastHLine(10, 400, 300, 0xFFE0); // YELLOW + sprite.drawFastVLine(160, 10, 460, 0xFFFF); // WHITE + sprite.setTextColor(0xFFFF, 0x0000); + sprite.setTextSize(3); + sprite.drawString("HELLO", 80, 430); + // corner + center 40x40 squares on top + sprite.fillRect(0, 0, 40, 40, 0xF800); // TL RED + sprite.fillRect(280, 0, 40, 40, 0x07E0); // TR GREEN + sprite.fillRect(0, 440, 40, 40, 0x001F); // BL BLUE + sprite.fillRect(280, 440, 40, 40, 0xFFE0); // BR YELLOW + sprite.fillRect(140, 220, 40, 40, 0xFFFF); // center WHITE + Serial.println("[diag-post] pushing sprite to panel via pushRawPixels (direct Arduino_GFX, one call)"); + // g_axs_panel is set in display_ui.cpp to &_tft_instance._panel. + // Single call to _agfx->writePixels avoids the multi-call RAMWRC + // chunking that's been producing stripes/chaos. + g_axs_panel->pushRawPixels(static_cast(sprite.getBuffer()), + 320u * 480u); + Serial.println("[diag-post] sprite pushed via direct path — halted"); + while (true) { delay(1000); } +#endif splashEnd = millis() + 2000; startWiFiDuringSplash(); setBacklight(brightness); From 8acb2495b58f62689252dabd78b23de679ed6e04 Mon Sep 17 00:00:00 2001 From: Niels Timmer Date: Thu, 23 Apr 2026 11:52:42 +0200 Subject: [PATCH 10/15] JC3248W535: byte-swap pixels in pushRawPixels for LSB-first wire order The AXS15231B in QSPI mode reads 16-bit pixel data LSB-first on the wire, not MSB-first as MIPI DCS specifies. Arduino_GFX's MSB_32_16_16_SET byte-swaps pixels from native LE to big-endian MSB-first before DMA, which produced rotated colors on this panel: RED -> BLUE, GREEN -> RED, BLUE -> GREEN, YELLOW -> MAGENTA (WHITE/BLACK unchanged because they're palindromic). Pre-swap each pixel with __builtin_bswap16 before handing it to _agfx->writePixels(), which cancels out the internal swap so the net wire byte order is LSB-first as the chip expects. Swap the buffer back after push so the sprite is left consistent for the caller and repeat pushes work. Verified on-device: solid colors now render in their requested hues. --- src/lgfx_panel_axs15231b_agfx.hpp | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/lgfx_panel_axs15231b_agfx.hpp b/src/lgfx_panel_axs15231b_agfx.hpp index f60668e..8216f23 100644 --- a/src/lgfx_panel_axs15231b_agfx.hpp +++ b/src/lgfx_panel_axs15231b_agfx.hpp @@ -247,10 +247,24 @@ class Panel_AXS15231B_AGFX : public Panel_Device { // ------------------------------------------------------------------------- void pushRawPixels(uint16_t* data, uint32_t length) { if (!_agfx || length == 0) return; + // Arduino_GFX's MSB_32_16_16_SET byte-swaps each pixel from native LE + // to big-endian MSB-first before DMA, which is the MIPI DCS convention + // for 16bpp pixel data. But this chip in QSPI mode evidently reads + // pixels LSB-first (observed: RED→BLUE, GREEN→RED, BLUE→GREEN, + // YELLOW→MAGENTA — exactly the pattern of byte-swapped RGB565). Pre- + // swap here to cancel Arduino_GFX's swap so the net wire byte order + // matches what the chip expects. Restore the sprite buffer afterwards + // so repeat pushes work. + for (uint32_t i = 0; i < length; ++i) { + data[i] = __builtin_bswap16(data[i]); + } _agfx->startWrite(); _agfx->writeAddrWindow(0, 0, _cfg.panel_width, _cfg.panel_height); _agfx->writePixels(data, length); _agfx->endWrite(); + for (uint32_t i = 0; i < length; ++i) { + data[i] = __builtin_bswap16(data[i]); + } } // ------------------------------------------------------------------------- From 1d1e2c632f1d4bef335a68e9ea63cd4456739915 Mon Sep 17 00:00:00 2001 From: Niels Timmer Date: Thu, 23 Apr 2026 13:16:26 +0200 Subject: [PATCH 11/15] JC3248W535: wire sprite-backed rendering into BambuHelper's normal loop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On JC3248W535 the display can only render correct pixels via a full-frame raster flush (arbitrary Y is unaddressable in QSPI mode, and the chip chokes on chunked multi-call pushes). Stand up a 320x480 16bpp PSRAM LGFX_Sprite on this board, retarget the global `tft` pointer to it at init, and flush to the panel once per loop() tick via pushRawPixels(). All existing tft.xxx() call sites keep working unmodified. Make `tft` a `#define tft (*tft_ptr)` macro so the retarget is effective at runtime — the previous C++ reference was bound at static-init time to the panel and couldn't be rebound. Helper files (display_anim, display_gauges, icons) used `tft` as a function-parameter name which collided with the macro; renamed those parameters to `gfx`. No behaviour change on any other board — `flushFrame()` is a no-op unless BOARD_IS_JC3248W535 is set. Force native portrait (rotation=0) on JC3248W535 — the sprite-push path doesn't handle rotated framebuffers yet. dispSettings.rotation is ignored on this board for now. Drops the DIAG_LGFX_POST_INIT diagnostic scaffolding that got us here. --- platformio.ini | 4 - src/display_anim.cpp | 44 +++---- src/display_anim.h | 8 +- src/display_gauges.cpp | 256 ++++++++++++++++++++--------------------- src/display_gauges.h | 16 +-- src/display_ui.cpp | 47 +++++++- src/display_ui.h | 15 ++- src/icons.h | 8 +- src/main.cpp | 59 ++-------- 9 files changed, 234 insertions(+), 223 deletions(-) diff --git a/platformio.ini b/platformio.ini index 3320e41..a673e4f 100644 --- a/platformio.ini +++ b/platformio.ini @@ -172,10 +172,6 @@ build_flags = -D USE_AXS_TOUCH=1 -D AXS_TOUCH_SDA=4 -D AXS_TOUCH_SCL=8 - ; --- DIAG re-test: after init cascade + initDisplay's fillScreen(CLR_BG), - ; run 5 tft.fillScreen()s and halt. Clean test of LGFX path with wrapper - ; fix applied (no sanity fillScreens in init()). - -D DIAG_LGFX_POST_INIT=1 ; ============================================================================= ; jc3248w535_skel — standalone test binary using the axs15231b-lovyangfx diff --git a/src/display_anim.cpp b/src/display_anim.cpp index a41fb1a..666aafe 100644 --- a/src/display_anim.cpp +++ b/src/display_anim.cpp @@ -12,18 +12,18 @@ // --------------------------------------------------------------------------- static uint16_t spinnerAngle = 0; -void drawSpinner(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t radius, +void drawSpinner(lgfx::LovyanGFX& gfx, int16_t cx, int16_t cy, int16_t radius, uint16_t color) { // Erase previous arc segment (handle wrap-around) uint16_t prevStart = (spinnerAngle + 360 - 12) % 360; uint16_t prevEnd = (prevStart + 60) % 360; if (prevEnd > prevStart) { - tft.drawArc(cx, cy, radius, radius - 4, + gfx.drawArc(cx, cy, radius, radius - 4, prevStart, prevEnd, CLR_BG); } else { - tft.drawArc(cx, cy, radius, radius - 4, + gfx.drawArc(cx, cy, radius, radius - 4, prevStart, 360, CLR_BG); - tft.drawArc(cx, cy, radius, radius - 4, + gfx.drawArc(cx, cy, radius, radius - 4, 0, prevEnd, CLR_BG); } @@ -34,12 +34,12 @@ void drawSpinner(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t radius, // Draw arc segment (handle wrap-around) if (arcEnd > arcStart) { - tft.drawArc(cx, cy, radius, radius - 4, + gfx.drawArc(cx, cy, radius, radius - 4, arcStart, arcEnd, color); } else { - tft.drawArc(cx, cy, radius, radius - 4, + gfx.drawArc(cx, cy, radius, radius - 4, arcStart, 360, color); - tft.drawArc(cx, cy, radius, radius - 4, + gfx.drawArc(cx, cy, radius, radius - 4, 0, arcEnd, color); } } @@ -47,27 +47,27 @@ void drawSpinner(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t radius, // --------------------------------------------------------------------------- // Animated dots "..." // --------------------------------------------------------------------------- -void drawAnimDots(lgfx::LovyanGFX& tft, int16_t x, int16_t y, uint16_t color) { +void drawAnimDots(lgfx::LovyanGFX& gfx, int16_t x, int16_t y, uint16_t color) { unsigned long ms = millis(); int phase = (ms / 400) % 4; - tft.setTextFont(2); - tft.setTextDatum(TL_DATUM); + gfx.setTextFont(2); + gfx.setTextDatum(TL_DATUM); for (int i = 0; i < 3; i++) { uint16_t dotColor = (i < phase) ? color : CLR_TEXT_DARK; - tft.setTextColor(dotColor, CLR_BG); - tft.drawString(".", x + i * 8, y); + gfx.setTextColor(dotColor, CLR_BG); + gfx.drawString(".", x + i * 8, y); } } // --------------------------------------------------------------------------- // Indeterminate slide bar — a glowing segment slides back and forth // --------------------------------------------------------------------------- -void drawSlideBar(lgfx::LovyanGFX& tft, int16_t x, int16_t y, int16_t w, int16_t h, +void drawSlideBar(lgfx::LovyanGFX& gfx, int16_t x, int16_t y, int16_t w, int16_t h, uint16_t color, uint16_t trackColor) { // Draw track (also erases previous segment position) - tft.fillRoundRect(x, y, w, h, h / 2, trackColor); + gfx.fillRoundRect(x, y, w, h, h / 2, trackColor); // Segment: 25% of bar width, bounces smoothly using sine const int16_t segW = w / 4; @@ -75,7 +75,7 @@ void drawSlideBar(lgfx::LovyanGFX& tft, int16_t x, int16_t y, int16_t w, int16_t float pos = (sinf(t * 2.0f * PI - PI / 2.0f) + 1.0f) / 2.0f; // 0..1 int16_t segX = x + (int16_t)(pos * (float)(w - segW)); - tft.fillRoundRect(segX, y, segW, h, h / 2, color); + gfx.fillRoundRect(segX, y, segW, h, h / 2, color); } // --------------------------------------------------------------------------- @@ -95,7 +95,7 @@ static unsigned long completionStart = 0; static bool completionDone = false; static int16_t prevRing = 0; -void drawCompletionAnim(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, bool reset) { +void drawCompletionAnim(lgfx::LovyanGFX& gfx, int16_t cx, int16_t cy, bool reset) { if (reset) { completionStart = millis(); completionDone = false; @@ -113,22 +113,22 @@ void drawCompletionAnim(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, bool reset int16_t r = 10 + (elapsed * (finalR - 10)) / 400; // Erase previous ring if (prevRing > 0 && prevRing != r) { - tft.drawArc(cx, cy, prevRing, prevRing - 3, 0, 360, CLR_BG); + gfx.drawArc(cx, cy, prevRing, prevRing - 3, 0, 360, CLR_BG); } - tft.drawArc(cx, cy, r, r - 3, 0, 360, CLR_GREEN); + gfx.drawArc(cx, cy, r, r - 3, 0, 360, CLR_GREEN); prevRing = r; } // Phase 2 (400-600ms): settle to final ring else if (elapsed < 600) { - tft.drawArc(cx, cy, finalR, finalR - 4, 0, 360, CLR_GREEN); + gfx.drawArc(cx, cy, finalR, finalR - 4, 0, 360, CLR_GREEN); } // Phase 3 (600ms+): static ring + large checkmark, done else { - tft.drawArc(cx, cy, finalR, finalR - 4, 0, 360, CLR_GREEN); + gfx.drawArc(cx, cy, finalR, finalR - 4, 0, 360, CLR_GREEN); // Clear center for checkmark - tft.fillCircle(cx, cy, finalR - 5, CLR_BG); + gfx.fillCircle(cx, cy, finalR - 5, CLR_BG); // Draw 32x32 checkmark centered - drawIcon32(tft, cx - 16, cy - 16, icon_check_32, CLR_GREEN); + drawIcon32(gfx, cx - 16, cy - 16, icon_check_32, CLR_GREEN); completionDone = true; } } diff --git a/src/display_anim.h b/src/display_anim.h index 64a72ce..6da0d9e 100644 --- a/src/display_anim.h +++ b/src/display_anim.h @@ -4,20 +4,20 @@ #include // Rotating arc spinner for connecting screens -void drawSpinner(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t radius, +void drawSpinner(lgfx::LovyanGFX& gfx, int16_t cx, int16_t cy, int16_t radius, uint16_t color); // Animated dots "..." that cycle (call each frame, uses millis()) -void drawAnimDots(lgfx::LovyanGFX& tft, int16_t x, int16_t y, uint16_t color); +void drawAnimDots(lgfx::LovyanGFX& gfx, int16_t x, int16_t y, uint16_t color); // Pulsing glow on arc edge (returns brightness factor 0.5-1.0) float getPulseFactor(); // Indeterminate slide bar (call each frame — uses millis() internally) -void drawSlideBar(lgfx::LovyanGFX& tft, int16_t x, int16_t y, int16_t w, int16_t h, +void drawSlideBar(lgfx::LovyanGFX& gfx, int16_t x, int16_t y, int16_t w, int16_t h, uint16_t color, uint16_t trackColor); // Completion animation: expanding checkmark ring -void drawCompletionAnim(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, bool reset); +void drawCompletionAnim(lgfx::LovyanGFX& gfx, int16_t cx, int16_t cy, bool reset); #endif // DISPLAY_ANIM_H diff --git a/src/display_gauges.cpp b/src/display_gauges.cpp index bd306fa..78dd9bb 100644 --- a/src/display_gauges.cpp +++ b/src/display_gauges.cpp @@ -28,8 +28,8 @@ class ScopedWrite { // --------------------------------------------------------------------------- // H2-style LED progress bar // --------------------------------------------------------------------------- -void drawLedProgressBar(lgfx::LovyanGFX& tft, int16_t y, uint8_t progress) { - ScopedWrite sw(tft); +void drawLedProgressBar(lgfx::LovyanGFX& gfx, int16_t y, uint8_t progress) { + ScopedWrite sw(gfx); uint16_t bg = dispSettings.bgColor; uint16_t track = dispSettings.trackColor; @@ -37,7 +37,7 @@ void drawLedProgressBar(lgfx::LovyanGFX& tft, int16_t y, uint8_t progress) { const int16_t barH = LY_BAR_H; const int16_t barX = (SCREEN_W - barW) / 2; - tft.fillRect(barX, y, barW, barH, bg); + gfx.fillRect(barX, y, barW, barH, bg); if (progress == 0) return; @@ -46,17 +46,17 @@ void drawLedProgressBar(lgfx::LovyanGFX& tft, int16_t y, uint8_t progress) { uint16_t barColor = dispSettings.progress.arc; - tft.fillRoundRect(barX, y, fillW, barH, 2, barColor); + gfx.fillRoundRect(barX, y, fillW, barH, 2, barColor); uint16_t glowColor = alphaBlend565(160, CLR_TEXT, barColor); - tft.drawFastHLine(barX + 1, y + barH / 2, fillW - 2, glowColor); + gfx.drawFastHLine(barX + 1, y + barH / 2, fillW - 2, glowColor); if (fillW > 4 && progress < 100) { - tft.fillRect(barX + fillW - 3, y, 3, barH, glowColor); + gfx.fillRect(barX + fillW - 3, y, 3, barH, glowColor); } if (fillW < barW) { - tft.fillRoundRect(barX + fillW, y, barW - fillW, barH, 2, track); + gfx.fillRoundRect(barX + fillW, y, barW - fillW, barH, 2, track); } } @@ -73,7 +73,7 @@ static const uint16_t SHIMMER_INTERVAL = 25; // ms between steps (~40fps) static const uint16_t SHIMMER_PAUSE = 1200; // ms pause between sweeps static const int16_t SHIMMER_STEP = 3; // pixels per step -void tickProgressShimmer(lgfx::LovyanGFX& tft, int16_t y, uint8_t progress, bool printing) { +void tickProgressShimmer(lgfx::LovyanGFX& gfx, int16_t y, uint8_t progress, bool printing) { if (!dispSettings.animatedBar || !printing || progress == 0) return; unsigned long now = millis(); @@ -96,7 +96,7 @@ void tickProgressShimmer(lgfx::LovyanGFX& tft, int16_t y, uint8_t progress, bool uint16_t barColor = dispSettings.progress.arc; - ScopedWrite sw_(tft); + ScopedWrite sw_(gfx); // Erase previous shimmer position (redraw base bar segment) if (shimmerPos > 0) { @@ -104,7 +104,7 @@ void tickProgressShimmer(lgfx::LovyanGFX& tft, int16_t y, uint8_t progress, bool int16_t eraseW = SHIMMER_STEP; if (eraseX < barX) { eraseW -= (barX - eraseX); eraseX = barX; } if (eraseW > 0) { - tft.fillRect(eraseX, y, eraseW, barH, barColor); + gfx.fillRect(eraseX, y, eraseW, barH, barColor); } } @@ -118,11 +118,11 @@ void tickProgressShimmer(lgfx::LovyanGFX& tft, int16_t y, uint8_t progress, bool uint16_t mid = alphaBlend565(100, CLR_TEXT, barColor); // Edge pixels if (sw >= 3) { - tft.fillRect(sx, y, 2, barH, mid); - tft.fillRect(sx + 2, y, sw - 4 > 0 ? sw - 4 : 1, barH, bright); - if (sw > 4) tft.fillRect(sx + sw - 2, y, 2, barH, mid); + gfx.fillRect(sx, y, 2, barH, mid); + gfx.fillRect(sx + 2, y, sw - 4 > 0 ? sw - 4 : 1, barH, bright); + if (sw > 4) gfx.fillRect(sx + sw - 2, y, 2, barH, mid); } else { - tft.fillRect(sx, y, sw, barH, bright); + gfx.fillRect(sx, y, sw, barH, bright); } } @@ -133,10 +133,10 @@ void tickProgressShimmer(lgfx::LovyanGFX& tft, int16_t y, uint8_t progress, bool // Restore the tail int16_t tailX = barX + fillW - SHIMMER_W - SHIMMER_STEP; if (tailX < barX) tailX = barX; - tft.fillRect(tailX, y, barX + fillW - tailX, barH, barColor); + gfx.fillRect(tailX, y, barX + fillW - tailX, barH, barColor); // Re-draw center glow line uint16_t glowColor = alphaBlend565(160, CLR_TEXT, barColor); - tft.drawFastHLine(barX + 1, y + barH / 2, fillW - 2, glowColor); + gfx.drawFastHLine(barX + 1, y + barH / 2, fillW - 2, glowColor); shimmerPos = 0; shimmerPaused = true; @@ -147,7 +147,7 @@ void tickProgressShimmer(lgfx::LovyanGFX& tft, int16_t y, uint8_t progress, bool // --------------------------------------------------------------------------- // Helper: draw arc track + fill, handling decrease properly // --------------------------------------------------------------------------- -static void drawArcFillLegacy(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, +static void drawArcFillLegacy(lgfx::LovyanGFX& gfx, int16_t cx, int16_t cy, int16_t radius, int16_t thickness, uint16_t fillEnd, uint16_t fillColor, bool forceRedraw) { // Internal angles use TFT_eSPI convention: 0°=bottom (6 o'clock), clockwise. @@ -166,15 +166,15 @@ static void drawArcFillLegacy(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, float la1 = (float)((a1 + 90u) % 360u); if (la0 > la1) { // Arc crosses the 0° boundary — split into two segments - tft.drawArc(cx, cy, radius, radius - thickness, la0, 360.0f, color); - tft.drawArc(cx, cy, radius, radius - thickness, 0.0f, la1, color); + gfx.drawArc(cx, cy, radius, radius - thickness, la0, 360.0f, color); + gfx.drawArc(cx, cy, radius, radius - thickness, 0.0f, la1, color); } else { - tft.drawArc(cx, cy, radius, radius - thickness, la0, la1, color); + gfx.drawArc(cx, cy, radius, radius - thickness, la0, la1, color); } }; if (forceRedraw) { - tft.fillCircle(cx, cy, radius + 2, bg); + gfx.fillCircle(cx, cy, radius + 2, bg); arcDraw(startAngle, endAngle, track); } @@ -218,7 +218,7 @@ static inline float wedgeLineDistanceAA(float xpax, float ypay, return sqrtf(dx * dx + dy * dy) + h * dr; } -static void drawWedgeLineAA(lgfx::LovyanGFX& tft, +static void drawWedgeLineAA(lgfx::LovyanGFX& gfx, float ax, float ay, float bx, float by, float ar, float br, uint16_t fg_color, uint16_t bg_color) { @@ -234,8 +234,8 @@ static void drawWedgeLineAA(lgfx::LovyanGFX& tft, int32_t y0 = (int32_t)floorf(fminf(ay - ar, by - br)); int32_t y1 = (int32_t) ceilf(fmaxf(ay + ar, by + br)); - const int32_t maxX = (int32_t)tft.width() - 1; - const int32_t maxY = (int32_t)tft.height() - 1; + const int32_t maxX = (int32_t)gfx.width() - 1; + const int32_t maxY = (int32_t)gfx.height() - 1; if (x1 < 0 || y1 < 0 || x0 > maxX || y0 > maxY) return; if (x0 < 0) x0 = 0; if (y0 < 0) y0 = 0; @@ -254,12 +254,12 @@ static void drawWedgeLineAA(lgfx::LovyanGFX& tft, const float alpha = aaRadius - wedgeLineDistanceAA(xpax, ypay, bax, bay, rdt); if (alpha <= loAlphaTheshold) continue; if (alpha > hiAlphaTheshold) { - tft.drawPixel(xp, yp, fg_color); + gfx.drawPixel(xp, yp, fg_color); continue; } const uint8_t blendAlpha = (uint8_t)(alpha * pixelAlphaGain); if (blendAlpha == 0) continue; - tft.drawPixel(xp, yp, alphaBlend565(blendAlpha, fg_color, bg_color)); + gfx.drawPixel(xp, yp, alphaBlend565(blendAlpha, fg_color, bg_color)); } } } @@ -267,7 +267,7 @@ static void drawWedgeLineAA(lgfx::LovyanGFX& tft, // Scan-quadrant AA annulus slice. Port of TFT_eSPI::drawArc (smooth=true). // Angles: 0°=6 o'clock, clockwise, range 0-360. r=outer, ir=inner (inclusive). // Ends are NOT anti-aliased — caller adds radial AA wedges for smooth ends. -static void drawArcAA(lgfx::LovyanGFX& tft, int32_t x, int32_t y, +static void drawArcAA(lgfx::LovyanGFX& gfx, int32_t x, int32_t y, int32_t r, int32_t ir, uint32_t startAngle, uint32_t endAngle, uint16_t fg_color, uint16_t bg_color) { @@ -279,7 +279,7 @@ static void drawArcAA(lgfx::LovyanGFX& tft, int32_t x, int32_t y, if (r <= 0 || ir < 0) return; if (endAngle < startAngle) { - if (startAngle < 360) drawArcAA(tft, x, y, r, ir, startAngle, 360, fg_color, bg_color); + if (startAngle < 360) drawArcAA(gfx, x, y, r, ir, startAngle, 360, fg_color, bg_color); if (endAngle == 0) return; startAngle = 0; } @@ -354,48 +354,48 @@ static void drawArcAA(lgfx::LovyanGFX& tft, int32_t x, int32_t y, if (alpha < 16) continue; uint16_t pcol = alphaBlend565(alpha, fg_color, bg_color); slope = ((r - cy) << 16) / (r - cx); - if (slope <= startSlope[0] && slope >= endSlope[0]) tft.drawPixel(x + cx - r, y - cy + r, pcol); - if (slope >= startSlope[1] && slope <= endSlope[1]) tft.drawPixel(x + cx - r, y + cy - r, pcol); - if (slope <= startSlope[2] && slope >= endSlope[2]) tft.drawPixel(x - cx + r, y + cy - r, pcol); - if (slope <= endSlope[3] && slope >= startSlope[3]) tft.drawPixel(x - cx + r, y - cy + r, pcol); + if (slope <= startSlope[0] && slope >= endSlope[0]) gfx.drawPixel(x + cx - r, y - cy + r, pcol); + if (slope >= startSlope[1] && slope <= endSlope[1]) gfx.drawPixel(x + cx - r, y + cy - r, pcol); + if (slope <= startSlope[2] && slope >= endSlope[2]) gfx.drawPixel(x - cx + r, y + cy - r, pcol); + if (slope <= endSlope[3] && slope >= startSlope[3]) gfx.drawPixel(x - cx + r, y - cy + r, pcol); } - if (len[0]) tft.drawFastHLine(x + xst[0] - len[0] + 1 - r, y - cy + r, len[0], fg_color); - if (len[1]) tft.drawFastHLine(x + xst[1] - len[1] + 1 - r, y + cy - r, len[1], fg_color); - if (len[2]) tft.drawFastHLine(x - xst[2] + r, y + cy - r, len[2], fg_color); - if (len[3]) tft.drawFastHLine(x - xst[3] + r, y - cy + r, len[3], fg_color); + if (len[0]) gfx.drawFastHLine(x + xst[0] - len[0] + 1 - r, y - cy + r, len[0], fg_color); + if (len[1]) gfx.drawFastHLine(x + xst[1] - len[1] + 1 - r, y + cy - r, len[1], fg_color); + if (len[2]) gfx.drawFastHLine(x - xst[2] + r, y + cy - r, len[2], fg_color); + if (len[3]) gfx.drawFastHLine(x - xst[3] + r, y - cy + r, len[3], fg_color); } - if (startAngle == 0 || endAngle == 360) tft.drawFastVLine(x, y + r - w, w, fg_color); - if (startAngle <= 90 && endAngle >= 90) tft.drawFastHLine(x - r + 1, y, w, fg_color); - if (startAngle <= 180 && endAngle >= 180) tft.drawFastVLine(x, y - r + 1, w, fg_color); - if (startAngle <= 270 && endAngle >= 270) tft.drawFastHLine(x + r - w, y, w, fg_color); + if (startAngle == 0 || endAngle == 360) gfx.drawFastVLine(x, y + r - w, w, fg_color); + if (startAngle <= 90 && endAngle >= 90) gfx.drawFastHLine(x - r + 1, y, w, fg_color); + if (startAngle <= 180 && endAngle >= 180) gfx.drawFastVLine(x, y - r + 1, w, fg_color); + if (startAngle <= 270 && endAngle >= 270) gfx.drawFastHLine(x + r - w, y, w, fg_color); } -static void drawArcCapAA(lgfx::LovyanGFX& tft, int32_t x, int32_t y, +static void drawArcCapAA(lgfx::LovyanGFX& gfx, int32_t x, int32_t y, int32_t r, int32_t ir, uint32_t angle, uint16_t fg_color, uint16_t bg_color) { constexpr float deg2rad = 3.14159265358979f / 180.0f; const float sx = -sinf(angle * deg2rad); const float sy = +cosf(angle * deg2rad); - drawWedgeLineAA(tft, + drawWedgeLineAA(gfx, sx * ir + x, sy * ir + y, sx * r + x, sy * r + y, 0.3f, 0.3f, fg_color, bg_color); } -static void drawArcSegmentAA(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, +static void drawArcSegmentAA(lgfx::LovyanGFX& gfx, int16_t cx, int16_t cy, int16_t radius, int16_t innerRadius, uint16_t a0, uint16_t a1, uint16_t fg_color, uint16_t bg_color, bool drawStartCap, bool drawEndCap, uint16_t startCapBg, uint16_t endCapBg) { if (a1 <= a0) return; - if (drawStartCap) drawArcCapAA(tft, cx, cy, radius, innerRadius, a0, fg_color, startCapBg); - if (drawEndCap) drawArcCapAA(tft, cx, cy, radius, innerRadius, a1, fg_color, endCapBg); - drawArcAA(tft, cx, cy, radius, innerRadius, a0, a1, fg_color, bg_color); + if (drawStartCap) drawArcCapAA(gfx, cx, cy, radius, innerRadius, a0, fg_color, startCapBg); + if (drawEndCap) drawArcCapAA(gfx, cx, cy, radius, innerRadius, a1, fg_color, endCapBg); + drawArcAA(gfx, cx, cy, radius, innerRadius, a0, a1, fg_color, bg_color); } -static void drawArcFill(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, +static void drawArcFill(lgfx::LovyanGFX& gfx, int16_t cx, int16_t cy, int16_t radius, int16_t thickness, uint16_t fillEnd, uint16_t fillColor, bool forceRedraw) { const uint16_t startAngle = 60; @@ -407,19 +407,19 @@ static void drawArcFill(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, const int16_t innerRadius = radius - thickness; if (forceRedraw) { - tft.fillCircle(cx, cy, radius + 2, bg); - drawArcSegmentAA(tft, cx, cy, radius, innerRadius, + gfx.fillCircle(cx, cy, radius + 2, bg); + drawArcSegmentAA(gfx, cx, cy, radius, innerRadius, startAngle, endAngle, track, bg, true, true, bg, bg); } if (clampedFillEnd > startAngle) { - drawArcSegmentAA(tft, cx, cy, radius, innerRadius, + drawArcSegmentAA(gfx, cx, cy, radius, innerRadius, startAngle, clampedFillEnd, fillColor, bg, true, true, bg, (clampedFillEnd < endAngle) ? track : bg); } if (clampedFillEnd < endAngle) { - drawArcSegmentAA(tft, cx, cy, radius, innerRadius, + drawArcSegmentAA(gfx, cx, cy, radius, innerRadius, clampedFillEnd, endAngle, track, bg, false, true, bg, bg); } @@ -428,10 +428,10 @@ static void drawArcFill(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, // --------------------------------------------------------------------------- // Helper: clear gauge center and prepare for text // --------------------------------------------------------------------------- -static void clearGaugeCenter(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, +static void clearGaugeCenter(lgfx::LovyanGFX& gfx, int16_t cx, int16_t cy, int16_t radius, int16_t thickness) { int16_t textR = radius - thickness - 1; - tft.fillCircle(cx, cy, textR, dispSettings.bgColor); + gfx.fillCircle(cx, cy, textR, dispSettings.bgColor); } // --------------------------------------------------------------------------- @@ -493,10 +493,10 @@ void resetGaugeTextCache() { // --------------------------------------------------------------------------- // Main progress arc // --------------------------------------------------------------------------- -void drawProgressArc(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t radius, +void drawProgressArc(lgfx::LovyanGFX& gfx, int16_t cx, int16_t cy, int16_t radius, int16_t thickness, uint8_t progress, uint8_t prevProgress, uint16_t remainingMin, bool forceRedraw) { - ScopedWrite sw(tft); + ScopedWrite sw(gfx); const uint16_t startAngle = 60; const GaugeColors& gc = dispSettings.progress; uint16_t bg = dispSettings.bgColor; @@ -504,7 +504,7 @@ void drawProgressArc(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t radiu uint16_t fillEnd = startAngle + (progress * 240) / 100; if (fillEnd > 300) fillEnd = 300; - drawArcFill(tft, cx, cy, radius, thickness, fillEnd, gc.arc, forceRedraw); + drawArcFill(gfx, cx, cy, radius, thickness, fillEnd, gc.arc, forceRedraw); bool compact = (radius < 50); @@ -524,22 +524,22 @@ void drawProgressArc(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t radiu // Only clear center + redraw text when displayed string actually changes if (gaugeTextChanged(cx, cy, pctBuf, timeBuf, forceRedraw)) { - clearGaugeCenter(tft, cx, cy, radius, thickness); + clearGaugeCenter(gfx, cx, cy, radius, thickness); - tft.setTextDatum(MC_DATUM); - tft.setTextColor(gc.value); - tft.setTextFont(4); - tft.drawString(pctBuf, cx, cy - (compact ? 4 : 8)); + gfx.setTextDatum(MC_DATUM); + gfx.setTextColor(gc.value); + gfx.setTextFont(4); + gfx.drawString(pctBuf, cx, cy - (compact ? 4 : 8)); - tft.setTextFont(compact ? 1 : 2); - tft.setTextColor(CLR_TEXT_DIM); - tft.drawString(timeBuf, cx, cy + (compact ? 10 : 18)); + gfx.setTextFont(compact ? 1 : 2); + gfx.setTextColor(CLR_TEXT_DIM); + gfx.drawString(timeBuf, cx, cy + (compact ? 10 : 18)); if (compact) { bool sm = dispSettings.smallLabels; - tft.setTextFont(sm ? 1 : 2); - tft.setTextColor(gc.label, bg); - tft.drawString("Progress", cx, cy + radius + (sm ? 3 : -1)); + gfx.setTextFont(sm ? 1 : 2); + gfx.setTextColor(gc.label, bg); + gfx.drawString("Progress", cx, cy + radius + (sm ? 3 : -1)); } } } @@ -547,12 +547,12 @@ void drawProgressArc(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t radiu // --------------------------------------------------------------------------- // Temperature arc gauge // --------------------------------------------------------------------------- -void drawTempGauge(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t radius, +void drawTempGauge(lgfx::LovyanGFX& gfx, int16_t cx, int16_t cy, int16_t radius, float current, float target, float maxTemp, uint16_t accentColor, const char* label, const uint8_t* icon, bool forceRedraw, const GaugeColors* colors, float arcValue) { - ScopedWrite sw(tft); + ScopedWrite sw(gfx); const uint16_t startAngle = 60; const int16_t thickness = 6; uint16_t bg = dispSettings.bgColor; @@ -576,7 +576,7 @@ void drawTempGauge(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t radius, uint16_t tempColor = arcColor; uint16_t drawFill = (ratio > 0.01f) ? fillEnd : startAngle; - drawArcFill(tft, cx, cy, radius, thickness, drawFill, tempColor, forceRedraw); + drawArcFill(gfx, cx, cy, radius, thickness, drawFill, tempColor, forceRedraw); // Build display strings char tempBuf[12], targetBuf[12]; @@ -587,34 +587,34 @@ void drawTempGauge(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t radius, // Only clear center + redraw text when displayed string actually changes if (gaugeTextChanged(cx, cy, tempBuf, targetBuf, forceRedraw)) { - clearGaugeCenter(tft, cx, cy, radius, thickness); + clearGaugeCenter(gfx, cx, cy, radius, thickness); - tft.setTextDatum(MC_DATUM); - tft.setTextFont(4); - tft.setTextColor(valColor); - tft.drawString(tempBuf, cx, hasTarget ? (cy - 4) : cy); + gfx.setTextDatum(MC_DATUM); + gfx.setTextFont(4); + gfx.setTextColor(valColor); + gfx.drawString(tempBuf, cx, hasTarget ? (cy - 4) : cy); if (hasTarget) { - tft.setTextFont(1); - tft.setTextColor(CLR_TEXT_DIM); - tft.drawString(targetBuf, cx, cy + 10); + gfx.setTextFont(1); + gfx.setTextColor(CLR_TEXT_DIM); + gfx.drawString(targetBuf, cx, cy + 10); } bool sm = dispSettings.smallLabels; - tft.setTextFont(sm ? 1 : 2); - tft.setTextColor(lblColor, bg); - tft.drawString(label, cx, cy + radius + (sm ? 3 : -1)); + gfx.setTextFont(sm ? 1 : 2); + gfx.setTextColor(lblColor, bg); + gfx.drawString(label, cx, cy + radius + (sm ? 3 : -1)); } } // --------------------------------------------------------------------------- // Fan speed gauge (0-100%) // --------------------------------------------------------------------------- -void drawFanGauge(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t radius, +void drawFanGauge(lgfx::LovyanGFX& gfx, int16_t cx, int16_t cy, int16_t radius, uint8_t percent, uint16_t accentColor, const char* label, bool forceRedraw, const GaugeColors* colors, float arcPercent) { - ScopedWrite sw(tft); + ScopedWrite sw(gfx); const uint16_t startAngle = 60; const int16_t thickness = 6; uint16_t bg = dispSettings.bgColor; @@ -636,7 +636,7 @@ void drawFanGauge(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t radius, } uint16_t drawFill = (arcVal > 0.5f) ? fillEnd : startAngle; - drawArcFill(tft, cx, cy, radius, thickness, drawFill, fanColor, forceRedraw); + drawArcFill(gfx, cx, cy, radius, thickness, drawFill, fanColor, forceRedraw); // Build display string char buf[8]; @@ -644,27 +644,27 @@ void drawFanGauge(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t radius, // Only clear center + redraw text when displayed value actually changes if (gaugeTextChanged(cx, cy, buf, "", forceRedraw)) { - clearGaugeCenter(tft, cx, cy, radius, thickness); + clearGaugeCenter(gfx, cx, cy, radius, thickness); - tft.setTextDatum(MC_DATUM); - tft.setTextFont(4); - tft.setTextColor(valColor); - tft.drawString(buf, cx, cy); + gfx.setTextDatum(MC_DATUM); + gfx.setTextFont(4); + gfx.setTextColor(valColor); + gfx.drawString(buf, cx, cy); bool sm = dispSettings.smallLabels; - tft.setTextFont(sm ? 1 : 2); - tft.setTextColor(lblColor, bg); - tft.drawString(label, cx, cy + radius + (sm ? 3 : -1)); + gfx.setTextFont(sm ? 1 : 2); + gfx.setTextColor(lblColor, bg); + gfx.drawString(label, cx, cy + radius + (sm ? 3 : -1)); } } // --------------------------------------------------------------------------- // AMS humidity gauge (percentage from humidityRaw, color from humidity level) // --------------------------------------------------------------------------- -void drawHumidityGauge(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t radius, +void drawHumidityGauge(lgfx::LovyanGFX& gfx, int16_t cx, int16_t cy, int16_t radius, uint8_t humidityRaw, uint8_t humidityLevel, bool present, const char* label, bool forceRedraw) { - ScopedWrite sw(tft); + ScopedWrite sw(gfx); const uint16_t startAngle = 60; const int16_t thickness = 6; uint16_t bg = dispSettings.bgColor; @@ -688,7 +688,7 @@ void drawHumidityGauge(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t rad } uint16_t drawFill = (pct > 0) ? fillEnd : startAngle; - drawArcFill(tft, cx, cy, radius, thickness, drawFill, arcColor, forceRedraw); + drawArcFill(gfx, cx, cy, radius, thickness, drawFill, arcColor, forceRedraw); // Build display string char buf[8]; @@ -699,27 +699,27 @@ void drawHumidityGauge(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t rad } if (gaugeTextChanged(cx, cy, buf, "", forceRedraw)) { - clearGaugeCenter(tft, cx, cy, radius, thickness); + clearGaugeCenter(gfx, cx, cy, radius, thickness); - tft.setTextDatum(MC_DATUM); - tft.setTextFont(4); - tft.setTextColor(present ? CLR_TEXT : CLR_TEXT_DIM); - tft.drawString(buf, cx, cy); + gfx.setTextDatum(MC_DATUM); + gfx.setTextFont(4); + gfx.setTextColor(present ? CLR_TEXT : CLR_TEXT_DIM); + gfx.drawString(buf, cx, cy); bool sm = dispSettings.smallLabels; - tft.setTextFont(sm ? 1 : 2); - tft.setTextColor(arcColor, bg); - tft.drawString(label, cx, cy + radius + (sm ? 3 : -1)); + gfx.setTextFont(sm ? 1 : 2); + gfx.setTextColor(arcColor, bg); + gfx.drawString(label, cx, cy + radius + (sm ? 3 : -1)); } } // --------------------------------------------------------------------------- // Layer progress gauge (current / total) // --------------------------------------------------------------------------- -void drawLayerGauge(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t radius, +void drawLayerGauge(lgfx::LovyanGFX& gfx, int16_t cx, int16_t cy, int16_t radius, int16_t thickness, uint16_t layerNum, uint16_t totalLayers, bool forceRedraw) { - ScopedWrite sw(tft); + ScopedWrite sw(gfx); const uint16_t startAngle = 60; uint16_t bg = dispSettings.bgColor; uint16_t arcColor = dispSettings.progress.arc; @@ -731,7 +731,7 @@ void drawLayerGauge(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t radius if (fillEnd > 300) fillEnd = 300; uint16_t drawFill = (ratio > 0.01f) ? fillEnd : startAngle; - drawArcFill(tft, cx, cy, radius, thickness, drawFill, arcColor, forceRedraw); + drawArcFill(gfx, cx, cy, radius, thickness, drawFill, arcColor, forceRedraw); // Build display strings - use smaller font for large numbers char layerBuf[12], totalBuf[12]; @@ -743,42 +743,42 @@ void drawLayerGauge(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t radius } if (gaugeTextChanged(cx, cy, layerBuf, totalBuf, forceRedraw)) { - clearGaugeCenter(tft, cx, cy, radius, thickness); + clearGaugeCenter(gfx, cx, cy, radius, thickness); - tft.setTextDatum(MC_DATUM); + gfx.setTextDatum(MC_DATUM); // Pick font size based on digit count to fit inside gauge bool hasTot = (totalLayers > 0); int digits = strlen(layerBuf) + strlen(totalBuf); bool useSmall = (digits > 7); - tft.setTextFont(useSmall ? 2 : 4); - tft.setTextColor(CLR_TEXT); - tft.drawString(layerBuf, cx, hasTot ? (cy - 4) : cy); + gfx.setTextFont(useSmall ? 2 : 4); + gfx.setTextColor(CLR_TEXT); + gfx.drawString(layerBuf, cx, hasTot ? (cy - 4) : cy); if (hasTot) { - tft.setTextFont(useSmall ? 1 : 2); - tft.setTextColor(CLR_TEXT_DIM); - tft.drawString(totalBuf, cx, cy + (useSmall ? 8 : 10)); + gfx.setTextFont(useSmall ? 1 : 2); + gfx.setTextColor(CLR_TEXT_DIM); + gfx.drawString(totalBuf, cx, cy + (useSmall ? 8 : 10)); } bool sm = dispSettings.smallLabels; - tft.setTextFont(sm ? 1 : 2); - tft.setTextColor(arcColor, bg); - tft.drawString("Layer", cx, cy + radius + (sm ? 3 : -1)); + gfx.setTextFont(sm ? 1 : 2); + gfx.setTextColor(arcColor, bg); + gfx.drawString("Layer", cx, cy + radius + (sm ? 3 : -1)); } } // --------------------------------------------------------------------------- // Clock widget - shows current time HH:MM inside a track ring // --------------------------------------------------------------------------- -void drawClockWidget(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t radius, +void drawClockWidget(lgfx::LovyanGFX& gfx, int16_t cx, int16_t cy, int16_t radius, int16_t thickness, bool forceRedraw) { - ScopedWrite sw(tft); + ScopedWrite sw(gfx); uint16_t bg = dispSettings.bgColor; if (forceRedraw) { - tft.fillCircle(cx, cy, radius + 2, bg); + gfx.fillCircle(cx, cy, radius + 2, bg); } // Get current time @@ -800,16 +800,16 @@ void drawClockWidget(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t radiu } if (gaugeTextChanged(cx, cy, timeBuf, "", forceRedraw)) { - tft.fillCircle(cx, cy, radius - 1, bg); + gfx.fillCircle(cx, cy, radius - 1, bg); - tft.setTextDatum(MC_DATUM); - tft.setTextFont(4); - tft.setTextColor(CLR_TEXT); - tft.drawString(timeBuf, cx, cy); + gfx.setTextDatum(MC_DATUM); + gfx.setTextFont(4); + gfx.setTextColor(CLR_TEXT); + gfx.drawString(timeBuf, cx, cy); bool sm = dispSettings.smallLabels; - tft.setTextFont(sm ? 1 : 2); - tft.setTextColor(CLR_TEXT_DIM, bg); - tft.drawString("Clock", cx, cy + radius + (sm ? 3 : -1)); + gfx.setTextFont(sm ? 1 : 2); + gfx.setTextColor(CLR_TEXT_DIM, bg); + gfx.drawString("Clock", cx, cy + radius + (sm ? 3 : -1)); } } diff --git a/src/display_gauges.h b/src/display_gauges.h index 454e791..a5ee311 100644 --- a/src/display_gauges.h +++ b/src/display_gauges.h @@ -6,19 +6,19 @@ struct GaugeColors; // forward declaration from settings.h // Draw H2-style LED progress bar (full-width, top of screen) -void drawLedProgressBar(lgfx::LovyanGFX& tft, int16_t y, uint8_t progress); +void drawLedProgressBar(lgfx::LovyanGFX& gfx, int16_t y, uint8_t progress); // Shimmer animation tick — call from loop(), runs at its own cadence -void tickProgressShimmer(lgfx::LovyanGFX& tft, int16_t y, uint8_t progress, bool printing); +void tickProgressShimmer(lgfx::LovyanGFX& gfx, int16_t y, uint8_t progress, bool printing); // Draw progress arc with percentage and time in center -void drawProgressArc(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t radius, +void drawProgressArc(lgfx::LovyanGFX& gfx, int16_t cx, int16_t cy, int16_t radius, int16_t thickness, uint8_t progress, uint8_t prevProgress, uint16_t remainingMin, bool forceRedraw); // Draw temperature arc gauge with current/target // arcValue: smooth value for arc position, current: actual value for text display -void drawTempGauge(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t radius, +void drawTempGauge(lgfx::LovyanGFX& gfx, int16_t cx, int16_t cy, int16_t radius, float current, float target, float maxTemp, uint16_t accentColor, const char* label, const uint8_t* icon, bool forceRedraw, @@ -27,22 +27,22 @@ void drawTempGauge(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t radius, // Draw fan speed gauge (0-100%) // arcPercent: smooth value for arc position (-1 = use percent) -void drawFanGauge(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t radius, +void drawFanGauge(lgfx::LovyanGFX& gfx, int16_t cx, int16_t cy, int16_t radius, uint8_t percent, uint16_t accentColor, const char* label, bool forceRedraw, const GaugeColors* colors = nullptr, float arcPercent = -1.0f); // Draw clock widget (HH:MM inside track ring) -void drawClockWidget(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t radius, +void drawClockWidget(lgfx::LovyanGFX& gfx, int16_t cx, int16_t cy, int16_t radius, int16_t thickness, bool forceRedraw); // Draw AMS humidity gauge (humidityRaw % with color from humidity level) -void drawHumidityGauge(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t radius, +void drawHumidityGauge(lgfx::LovyanGFX& gfx, int16_t cx, int16_t cy, int16_t radius, uint8_t humidityRaw, uint8_t humidityLevel, bool present, const char* label, bool forceRedraw); // Draw layer progress gauge (current / total layers) -void drawLayerGauge(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t radius, +void drawLayerGauge(lgfx::LovyanGFX& gfx, int16_t cx, int16_t cy, int16_t radius, int16_t thickness, uint16_t layerNum, uint16_t totalLayers, bool forceRedraw); diff --git a/src/display_ui.cpp b/src/display_ui.cpp index 52a0f30..c92d4a3 100644 --- a/src/display_ui.cpp +++ b/src/display_ui.cpp @@ -275,16 +275,35 @@ static LGFX_C3 _tft_instance; // populated with either the V2 or Classic panel in initDisplay(), so method // calls via this reference/pointer dispatch to whichever variant was chosen. lgfx::LovyanGFX* tft_ptr = &_tft_instance; -lgfx::LovyanGFX& tft = *tft_ptr; +// `tft` is now a macro in display_ui.h — `#define tft (*tft_ptr)` — so +// every call site re-dereferences the pointer and picks up runtime +// retargeting to the JC3248W535 PSRAM sprite. // Direct panel pointer for JC3248W535 sprite escape-hatch; nullptr on all // other boards so the extern declaration in display_ui.h is always satisfied. #if defined(BOARD_IS_JC3248W535) lgfx::Panel_AXS15231B_AGFX* g_axs_panel = _tft_instance.panelAXS(); + +// Full-frame PSRAM sprite. All BambuHelper draws are redirected here in +// initDisplay() (via tft_ptr), then flushed to the panel once per loop() +// tick via flushFrame(). The AXS15231B in QSPI mode cannot address +// arbitrary Y per draw (see lgfx_panel_axs15231b_agfx.hpp), so a +// framebuffer-and-single-raster-flush is the only reliable render path. +static lgfx::LGFX_Sprite _frame_sprite(&_tft_instance); #else lgfx::Panel_AXS15231B_AGFX* g_axs_panel = nullptr; #endif +void flushFrame() { +#if defined(BOARD_IS_JC3248W535) + if (g_axs_panel && _frame_sprite.getBuffer()) { + g_axs_panel->pushRawPixels( + static_cast(_frame_sprite.getBuffer()), + 320u * 480u); + } +#endif +} + // Use user-configured bg color instead of hardcoded CLR_BG #undef CLR_BG #define CLR_BG (dispSettings.bgColor) @@ -405,7 +424,14 @@ void initDisplay() { tft.setRotation(0); tft.fillScreen(TFT_BLACK); #endif +#if defined(BOARD_IS_JC3248W535) + // Force native portrait on this panel — sprite-push doesn't handle rotated + // framebuffers (the wrapper's pushRawPixels assumes 320x480 raster order). + // dispSettings.rotation is ignored on this board for now. + tft.setRotation(0); +#else tft.setRotation(dispSettings.rotation); +#endif #if defined(DISPLAY_CYD) applyCydPanelInversion(); #elif defined(DISPLAY_240x320) @@ -415,6 +441,25 @@ void initDisplay() { tft.fillScreen(CLR_BG); Serial.println("Display: fillScreen done"); +#if defined(BOARD_IS_JC3248W535) + // Allocate 320x480x16bpp PSRAM sprite (300 KB) and redirect tft_ptr so all + // subsequent draws (splash, UI, refreshes) render into the sprite buffer. + // Panel cannot address arbitrary Y in QSPI mode — instead we flush the + // whole sprite to the panel once per loop tick via flushFrame(). + _frame_sprite.setPsram(true); + _frame_sprite.setColorDepth(16); + if (_frame_sprite.createSprite(320, 480)) { + _frame_sprite.setTextDatum(MC_DATUM); // match the tft defaults used below + _frame_sprite.fillScreen(CLR_BG); + tft_ptr = &_frame_sprite; + Serial.printf("Display: frame sprite 320x480 allocated in PSRAM, free=%u\n", + (unsigned)heap_caps_get_free_size(MALLOC_CAP_SPIRAM)); + flushFrame(); // push cleared sprite so panel shows CLR_BG during splash + } else { + Serial.println("Display: frame sprite alloc FAILED — will draw direct to panel (expect artifacts)"); + } +#endif + #if defined(TOUCH_CS) && !defined(USE_XPT2046) // LovyanGFX touch calibration diff --git a/src/display_ui.h b/src/display_ui.h index 8af7cc2..1d02d64 100644 --- a/src/display_ui.h +++ b/src/display_ui.h @@ -22,8 +22,11 @@ enum ScreenState { }; extern lgfx::LovyanGFX* tft_ptr; -// Convenience reference — all callers use `tft.method()` unchanged. -extern lgfx::LovyanGFX& tft; +// Macro (NOT a reference) so callers' `tft.method()` always dereferences the +// current value of `tft_ptr`. On JC3248W535 we retarget this pointer to a +// PSRAM sprite at runtime; a C++ reference would have been permanently +// bound to the panel at static-init time, defeating the redirection. +#define tft (*tft_ptr) // Direct pointer to the AXS15231B panel wrapper; only non-null on // BOARD_IS_JC3248W535 builds. Used by the sprite direct-push diagnostic. @@ -31,6 +34,14 @@ extern lgfx::Panel_AXS15231B_AGFX* g_axs_panel; void initDisplay(); void updateDisplay(); + +// Flush the off-screen framebuffer sprite to the panel in one contiguous +// raster write. No-op on boards that draw directly (all except +// BOARD_IS_JC3248W535, which uses a full-screen PSRAM sprite to work around +// the AXS15231B QSPI-mode addressing limits). Call once per loop tick after +// UI draws to commit the frame. +void flushFrame(); + void setScreenState(ScreenState state); ScreenState getScreenState(); void setBacklight(uint8_t level); diff --git a/src/icons.h b/src/icons.h index 396aa21..d634d9c 100644 --- a/src/icons.h +++ b/src/icons.h @@ -265,7 +265,7 @@ const uint8_t PROGMEM icon_lightning[] = { }; // Helper: draw a 16x16 1-bit icon at (x, y) with given color, transparent bg -inline void drawIcon16(lgfx::LovyanGFX& tft, int16_t x, int16_t y, +inline void drawIcon16(lgfx::LovyanGFX& gfx, int16_t x, int16_t y, const uint8_t* icon, uint16_t color) { for (int row = 0; row < 16; row++) { uint8_t b0 = pgm_read_byte(&icon[row * 2]); @@ -273,14 +273,14 @@ inline void drawIcon16(lgfx::LovyanGFX& tft, int16_t x, int16_t y, uint16_t bits = (b0 << 8) | b1; for (int col = 0; col < 16; col++) { if (bits & (0x8000 >> col)) { - tft.drawPixel(x + col, y + row, color); + gfx.drawPixel(x + col, y + row, color); } } } } // Helper: draw a 32x32 1-bit icon at (x, y) with given color, transparent bg -inline void drawIcon32(lgfx::LovyanGFX& tft, int16_t x, int16_t y, +inline void drawIcon32(lgfx::LovyanGFX& gfx, int16_t x, int16_t y, const uint8_t* icon, uint16_t color) { for (int row = 0; row < 32; row++) { uint32_t bits = ((uint32_t)pgm_read_byte(&icon[row * 4]) << 24) | @@ -289,7 +289,7 @@ inline void drawIcon32(lgfx::LovyanGFX& tft, int16_t x, int16_t y, (uint32_t)pgm_read_byte(&icon[row * 4 + 3]); for (int col = 0; col < 32; col++) { if (bits & (0x80000000UL >> col)) { - tft.drawPixel(x + col, y + row, color); + gfx.drawPixel(x + col, y + row, color); } } } diff --git a/src/main.cpp b/src/main.cpp index 82a5481..78832c4 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,9 +1,4 @@ #include -// Full panel definition needed for pushRawPixels escape-hatch (sprite diag). -// Must precede display_ui.h so the forward-declaration there is superseded. -#if defined(BOARD_IS_JC3248W535) -#include "lgfx_panel_axs15231b_agfx.hpp" -#endif #include "display_ui.h" #include "settings.h" #include "wifi_manager.h" @@ -454,57 +449,16 @@ void setup() { loadSettings(); initDisplay(); -#if defined(BOARD_IS_JC3248W535) && defined(DIAG_LGFX_POST_INIT) - // ---- SPRITE-BASED DIAGNOSTIC --------------------------------------------- - // The AXS15231B in QSPI mode cannot address arbitrary Y — every RAMWR - // resets the internal y-pointer to 0 within the CASET column window. - // Small draws at (x, y != 0) therefore always land at top-of-screen. - // Workaround: draw everything into an off-screen sprite in PSRAM, then - // push the whole sprite to the panel in ONE contiguous raster write. - // pushSprite issues a single setWindow(0,0,319,479) + bulk pixel write, - // which the chip handles correctly (all pixels are contiguous y from 0). - Serial.println("[diag-post] allocating 320x480 sprite in PSRAM"); - static lgfx::LGFX_Sprite sprite(&tft); - sprite.setPsram(true); - sprite.setColorDepth(16); - if (!sprite.createSprite(320, 480)) { - Serial.println("[diag-post] sprite alloc FAILED — halting"); - while (true) { delay(1000); } - } - Serial.printf("[diag-post] sprite alloc OK, free PSRAM=%u\n", - (unsigned)heap_caps_get_free_size(MALLOC_CAP_SPIRAM)); - // Draw the same test pattern into the sprite - sprite.fillScreen(0x0000); - sprite.fillRect(50, 50, 200, 100, 0xF800); // RED - sprite.fillRect(20, 200, 280, 40, 0x07E0); // GREEN - sprite.fillRect(0, 300, 320, 20, 0x001F); // BLUE - sprite.drawFastHLine(10, 400, 300, 0xFFE0); // YELLOW - sprite.drawFastVLine(160, 10, 460, 0xFFFF); // WHITE - sprite.setTextColor(0xFFFF, 0x0000); - sprite.setTextSize(3); - sprite.drawString("HELLO", 80, 430); - // corner + center 40x40 squares on top - sprite.fillRect(0, 0, 40, 40, 0xF800); // TL RED - sprite.fillRect(280, 0, 40, 40, 0x07E0); // TR GREEN - sprite.fillRect(0, 440, 40, 40, 0x001F); // BL BLUE - sprite.fillRect(280, 440, 40, 40, 0xFFE0); // BR YELLOW - sprite.fillRect(140, 220, 40, 40, 0xFFFF); // center WHITE - Serial.println("[diag-post] pushing sprite to panel via pushRawPixels (direct Arduino_GFX, one call)"); - // g_axs_panel is set in display_ui.cpp to &_tft_instance._panel. - // Single call to _agfx->writePixels avoids the multi-call RAMWRC - // chunking that's been producing stripes/chaos. - g_axs_panel->pushRawPixels(static_cast(sprite.getBuffer()), - 320u * 480u); - Serial.println("[diag-post] sprite pushed via direct path — halted"); - while (true) { delay(1000); } -#endif splashEnd = millis() + 2000; startWiFiDuringSplash(); setBacklight(brightness); } void loop() { - if (handleSplashPhase()) return; + if (handleSplashPhase()) { + flushFrame(); // commit splash draws to panel (no-op on non-JC boards) + return; + } handleWiFi(); handleWebServer(); @@ -529,4 +483,9 @@ void loop() { handleBambuMqtt(); handleRotation(); } + + // Commit the framebuffer sprite to the panel. On JC3248W535 this is a + // ~20ms QSPI push (300 KB @ 32MHz QIO); on all other boards it's a no-op + // since draws go directly to the panel. + flushFrame(); } From 1b989a5b06c683aaa64e90a51166b26883832d4d Mon Sep 17 00:00:00 2001 From: Niels Timmer Date: Thu, 23 Apr 2026 13:45:47 +0200 Subject: [PATCH 12/15] JC3248W535: default buttonType to BTN_TOUCHSCREEN when USE_AXS_TOUCH is set MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The auto-default-to-touchscreen branch in loadSettings() listed USE_CST816, USE_XPT2046, and TOUCH_CS but not USE_AXS_TOUCH, so JC3248W535 boards with no persisted btn_type would fall through to BTN_DISABLED and leave the built-in capacitive touch uninitialised until the user manually enabled it in the web UI. Add USE_AXS_TOUCH to the branch so fresh-NVS boards come up with touch ready. No effect on boards that already have btn_type saved in NVS — those keep their persisted value. Users with prior settings need to flip the option once in the web UI or clear NVS. --- src/settings.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/settings.cpp b/src/settings.cpp index bb332c0..70bb0aa 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -328,7 +328,7 @@ void loadSettings() { rotState.lastRotateMs = 0; // Button settings -#if defined(USE_CST816) || defined(USE_XPT2046) || defined(TOUCH_CS) +#if defined(USE_CST816) || defined(USE_XPT2046) || defined(USE_AXS_TOUCH) || defined(TOUCH_CS) buttonType = (ButtonType)prefs.getUChar("btn_type", BTN_TOUCHSCREEN); #else buttonType = (ButtonType)prefs.getUChar("btn_type", BTN_DISABLED); From ce5a3c1be0fe030287c908c920125aa8028f9248 Mon Sep 17 00:00:00 2001 From: Niels Timmer Date: Thu, 23 Apr 2026 15:36:56 +0200 Subject: [PATCH 13/15] JC3248W535: zero button pin on conflict with touch I2C / backlight / buzzer On the JC3248W535 the default button pin (4) is the same GPIO as AXS_TOUCH_SDA, so picking the push-button or TTP223 option with the default pin silently breaks the touchscreen bus. Mirror the existing sanitizeBuzzerPin() idiom: check buttonPin against BACKLIGHT_PIN, the active touch-bus pins, and buzzerSettings.pin on save and on initButton(); zero the pin on conflict so the button becomes a no-op instead of fighting a shared bus. --- src/button.cpp | 43 +++++++++++++++++++++++++++++++++++++++++++ src/button.h | 3 +++ src/settings.cpp | 2 ++ 3 files changed, 48 insertions(+) diff --git a/src/button.cpp b/src/button.cpp index 0514cc5..d42cfbc 100644 --- a/src/button.cpp +++ b/src/button.cpp @@ -1,5 +1,6 @@ #include "button.h" #include "settings.h" +#include "buzzer.h" #if defined(USE_XPT2046) #include @@ -72,8 +73,50 @@ static bool stableState = false; static unsigned long lastChangeMs = 0; static const unsigned long DEBOUNCE_MS = 50; +void sanitizeButtonPin() { + // Only the GPIO-backed button types use buttonPin. Touchscreen talks over + // a bus defined elsewhere and has no single pin to conflict. + if (buttonType != BTN_PUSH && buttonType != BTN_TOUCH) return; + if (buttonPin == 0) return; + + auto clash = [&](const char* what) { + Serial.printf("Button: pin %u conflicts with %s, disabling\n", + (unsigned)buttonPin, what); + buttonPin = 0; + }; + +#if defined(BACKLIGHT_PIN) && BACKLIGHT_PIN >= 0 + if (buttonPin == BACKLIGHT_PIN) { clash("backlight"); return; } +#endif +#if defined(USE_AXS_TOUCH) + if (buttonPin == AXS_TOUCH_SDA) { clash("AXS touch SDA"); return; } + if (buttonPin == AXS_TOUCH_SCL) { clash("AXS touch SCL"); return; } +#endif +#if defined(USE_CST816) + if (buttonPin == CST816_SDA) { clash("CST816 touch SDA"); return; } + if (buttonPin == CST816_SCL) { clash("CST816 touch SCL"); return; } + #if defined(CST816_IRQ) + if (buttonPin == CST816_IRQ) { clash("CST816 touch IRQ"); return; } + #endif + #if defined(CST816_RST) + if (buttonPin == CST816_RST) { clash("CST816 touch RST"); return; } + #endif +#endif +#if defined(USE_XPT2046) + if (buttonPin == TOUCH_CS) { clash("XPT2046 CS"); return; } + if (buttonPin == TOUCH_IRQ) { clash("XPT2046 IRQ"); return; } + if (buttonPin == TOUCH_MOSI) { clash("XPT2046 MOSI"); return; } + if (buttonPin == TOUCH_MISO) { clash("XPT2046 MISO"); return; } + if (buttonPin == TOUCH_CLK) { clash("XPT2046 CLK"); return; } +#endif + if (buzzerSettings.pin != 0 && buttonPin == buzzerSettings.pin) { + clash("buzzer"); return; + } +} + void initButton() { if (buttonType == BTN_DISABLED) return; + sanitizeButtonPin(); #if defined(USE_XPT2046) if (buttonType == BTN_TOUCHSCREEN) { touchSPI.begin(TOUCH_CLK, TOUCH_MISO, TOUCH_MOSI, TOUCH_CS); diff --git a/src/button.h b/src/button.h index fb3244b..1790326 100644 --- a/src/button.h +++ b/src/button.h @@ -5,5 +5,8 @@ void initButton(); bool wasButtonPressed(); // returns true once per press (edge-detected, debounced) +void sanitizeButtonPin(); // zero buttonPin if it conflicts with a reserved + // subsystem (backlight, touch bus, buzzer). No-op + // for touchscreen type. #endif // BUTTON_H diff --git a/src/settings.cpp b/src/settings.cpp index 70bb0aa..dc43d18 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -1,5 +1,6 @@ #include "settings.h" #include "config.h" +#include "button.h" #include "buzzer.h" #include "timezones.h" #include @@ -471,6 +472,7 @@ void saveRotationSettings() { } void saveButtonSettings() { + sanitizeButtonPin(); prefs.begin(NVS_NAMESPACE, false); prefs.putUChar("btn_type", buttonType); prefs.putUChar("btn_pin", buttonPin); From 1513d9cebb4185d8c61eec7e8164d4e05e1f7f55 Mon Sep 17 00:00:00 2001 From: Niels Timmer Date: Thu, 23 Apr 2026 15:45:46 +0200 Subject: [PATCH 14/15] JC3248W535: support runtime portrait rotation via sprite-side rotation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Users could select a rotation in the web UI but it was silently ignored on this board — initDisplay() force-set rotation 0 because the sprite-push path assumes a 320x480 raster order. Rotate the PSRAM sprite instead of the panel: panel MADCTL stays at 0 (preserving the RASET-skip and LSB-first byte-order invariants), and sprite storage dimensions stay fixed for even rotations so flushFrame() is unchanged. Landscape (1, 3) snaps to 0 with a Serial warning until a 480x320 layout exists. --- src/display_ui.cpp | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/display_ui.cpp b/src/display_ui.cpp index c92d4a3..4b77ec0 100644 --- a/src/display_ui.cpp +++ b/src/display_ui.cpp @@ -304,6 +304,21 @@ void flushFrame() { #endif } +// JC3248W535 currently only supports portrait rotations (0 and 2) in the +// sprite-push architecture — layout_320x480.h is hard-coded portrait and no +// landscape layout exists. Snap odd rotations to 0 with a Serial warning. +static uint8_t sanitizeRotation(uint8_t r) { +#if defined(BOARD_IS_JC3248W535) + if (r == 1 || r == 3) { + Serial.printf("Display: rotation %u unsupported on JC3248W535 " + "(landscape layout not yet available); snapping to 0\n", + (unsigned)r); + return 0; + } +#endif + return r; +} + // Use user-configured bg color instead of hardcoded CLR_BG #undef CLR_BG #define CLR_BG (dispSettings.bgColor) @@ -425,9 +440,9 @@ void initDisplay() { tft.fillScreen(TFT_BLACK); #endif #if defined(BOARD_IS_JC3248W535) - // Force native portrait on this panel — sprite-push doesn't handle rotated - // framebuffers (the wrapper's pushRawPixels assumes 320x480 raster order). - // dispSettings.rotation is ignored on this board for now. + // Panel MADCTL stays at 0 forever — RASET-skip + LSB-first byte-order + // invariants in pushRawPixels depend on native orientation. User-facing + // rotation is applied to the PSRAM sprite after tft_ptr is redirected. tft.setRotation(0); #else tft.setRotation(dispSettings.rotation); @@ -450,10 +465,11 @@ void initDisplay() { _frame_sprite.setColorDepth(16); if (_frame_sprite.createSprite(320, 480)) { _frame_sprite.setTextDatum(MC_DATUM); // match the tft defaults used below - _frame_sprite.fillScreen(CLR_BG); tft_ptr = &_frame_sprite; Serial.printf("Display: frame sprite 320x480 allocated in PSRAM, free=%u\n", (unsigned)heap_caps_get_free_size(MALLOC_CAP_SPIRAM)); + tft.setRotation(sanitizeRotation(dispSettings.rotation)); + tft.fillScreen(CLR_BG); flushFrame(); // push cleared sprite so panel shows CLR_BG during splash } else { Serial.println("Display: frame sprite alloc FAILED — will draw direct to panel (expect artifacts)"); @@ -493,7 +509,7 @@ void applyDisplaySettings() { tft.setRotation(0); tft.fillScreen(TFT_BLACK); #endif - tft.setRotation(dispSettings.rotation); + tft.setRotation(sanitizeRotation(dispSettings.rotation)); #if defined(DISPLAY_CYD) applyCydPanelInversion(); #elif defined(DISPLAY_240x320) From e4aaabd6ae31f884a271b3e0ebce387feb2b29c1 Mon Sep 17 00:00:00 2001 From: Niels Timmer Date: Thu, 23 Apr 2026 16:00:37 +0200 Subject: [PATCH 15/15] JC3248W535: drop diagnostic scaffolding before merge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The feature branch still carried investigation-era artifacts that should not land on main: - src/skeleton_test.cpp — standalone Arduino_GFX bring-up sketch, only useful while proving the driver worked on this hardware. - src/main_vendor.cpp + src/vendor/* — vendor ESP-IDF AXS15231B panel driver + custom QSPI shim used to isolate framing bugs. The production wrapper superseded it; the files have been dead code since the sprite-push architecture landed. - [env:jc3248w535_skel] / [env:jc3248w535_vendor] PlatformIO envs and their build_src_filter exclusion in [env:jc3248w535]. Also drop the "Option 3 from the skill" reference in the wrapper header — that numbering doesn't match the skill today and the comment reads better without it. No behaviour change on the production jc3248w535 env. --- platformio.ini | 52 ---- src/lgfx_panel_axs15231b_agfx.hpp | 12 +- src/main_vendor.cpp | 187 ------------- src/skeleton_test.cpp | 57 ---- src/vendor/esp_lcd_axs15231b.c | 392 --------------------------- src/vendor/esp_lcd_axs15231b.h | 208 -------------- src/vendor/esp_lcd_touch.h | 436 ------------------------------ src/vendor/my_panel_io.c | 160 ----------- src/vendor/my_panel_io.h | 25 -- 9 files changed, 6 insertions(+), 1523 deletions(-) delete mode 100644 src/main_vendor.cpp delete mode 100644 src/skeleton_test.cpp delete mode 100644 src/vendor/esp_lcd_axs15231b.c delete mode 100644 src/vendor/esp_lcd_axs15231b.h delete mode 100644 src/vendor/esp_lcd_touch.h delete mode 100644 src/vendor/my_panel_io.c delete mode 100644 src/vendor/my_panel_io.h diff --git a/platformio.ini b/platformio.ini index a673e4f..99a5d18 100644 --- a/platformio.ini +++ b/platformio.ini @@ -151,9 +151,6 @@ board_upload.flash_size = 16MB board_upload.maximum_size = 16777216 board_build.f_flash = 80000000L board_build.flash_mode = qio -; Exclude the skeleton_test and vendor-shim diagnostics — those are used only -; by jc3248w535_skel / jc3248w535_vendor and they define their own setup/loop. -build_src_filter = +<*> - - - lib_deps = ${common.lib_deps} moononournation/GFX Library for Arduino@~1.5.0 @@ -173,55 +170,6 @@ build_flags = -D AXS_TOUCH_SDA=4 -D AXS_TOUCH_SCL=8 -; ============================================================================= -; jc3248w535_skel — standalone test binary using the axs15231b-lovyangfx -; skill's skeleton verbatim. No BambuHelper code. Used to isolate whether -; the issue is in BambuHelper's integration or at the driver/hw level. -; ============================================================================= -[env:jc3248w535_skel] -extends = env:jc3248w535 -build_src_filter = -<*> + -lib_deps = - ${common.lib_deps} - moononournation/GFX Library for Arduino@~1.5.0 -build_flags = - -D BOARD_VARIANT=\"jc3248w535_skel\" - -D BOARD_IS_JC3248W535_SKEL=1 - -D BOARD_HAS_PSRAM=1 - -D ARDUINO_USB_CDC_ON_BOOT=1 - -; ============================================================================= -; jc3248w535_vendor — baseline diagnostic. Compiles the MANUFACTURER'S -; ESP-IDF AXS15231B panel driver verbatim (src/vendor/esp_lcd_axs15231b.*), -; wired up via a minimal Arduino sketch that calls esp_lcd_new_panel_io_spi + -; esp_lcd_new_panel_axs15231b + esp_lcd_panel_draw_bitmap directly. If this -; binary paints RED/GREEN/BLUE/WHITE/BLACK then we have proof the toolchain -; works and the vendor driver is correct on our hardware — our custom -; LovyanGFX Bus_QSPI can then be deleted in favour of wrapping this. -; -; REQUIRES: the vendor-shipped esp32s3 framework libs have been swapped into -; ~/.platformio/packages/framework-arduinoespressif32/tools/sdk/esp32s3 so -; that esp_lcd_panel_io_spi_config_t::flags.quad_mode is defined. Done -; manually earlier in the development session. -; ============================================================================= -[env:jc3248w535_vendor] -; Compiles the vendor ESP-IDF AXS15231B panel driver verbatim, wired up via a -; CUSTOM esp_lcd_panel_io_t shim (src/vendor/my_panel_io.c) that implements the -; QSPI framing ourselves — so we don't depend on stock arduino-esp32 having the -; quad_mode flag in esp_lcd_panel_io_spi_config_t (it doesn't in 3.0.17). -; -; This isolates "vendor setWindow + RAMWR/RAMWRC sequencing" from "our wire -; framing": the vendor panel driver handles init-table walking, CASET, RAMWR -; dispatch; our shim handles the actual SPI transactions. If this paints, our -; wire framing is fine and the previous bug was in our setWindow/draw logic. -extends = env:jc3248w535 -build_src_filter = -<*> + + + -build_flags = - -D BOARD_VARIANT=\"jc3248w535_vendor\" - -D BOARD_IS_JC3248W535_VENDOR=1 - -D BOARD_HAS_PSRAM=1 - -D ARDUINO_USB_CDC_ON_BOOT=1 - ; ============================================================================= ; ESP32-C3 Super Mini + ST7789 240x240 ; ============================================================================= diff --git a/src/lgfx_panel_axs15231b_agfx.hpp b/src/lgfx_panel_axs15231b_agfx.hpp index 8216f23..b3b3cf2 100644 --- a/src/lgfx_panel_axs15231b_agfx.hpp +++ b/src/lgfx_panel_axs15231b_agfx.hpp @@ -3,13 +3,13 @@ // drawing primitives to it. Lets the whole of BambuHelper keep calling // `tft.fillRect()`, `tft.drawString()`, etc. on a LovyanGFX reference without // changing any call sites, while the actual QSPI traffic is handled by the -// proven-working Arduino_GFX driver. +// proven-working moononournation/Arduino_GFX driver. // -// Why this exists: mainline LovyanGFX has no AXS15231B panel class and our -// hand-rolled Panel_AXS15231B (src/lgfx_panel_axs15231b.hpp) never worked on -// this hardware. Arduino_GFX does work (see src/skeleton_test.cpp). This -// wrapper is "Option 3" from the jc3248w535 skill — wrap a proven external -// driver inside a LovyanGFX Panel subclass. +// Why this exists: mainline LovyanGFX has no AXS15231B panel class. A +// hand-rolled one never produced correct pixels on this hardware, and +// Arduino_GFX's Arduino_AXS15231B drives the chip correctly out of the box. +// Wrapping it inside a LovyanGFX Panel subclass keeps the rest of the app +// on a single graphics API. #pragma once diff --git a/src/main_vendor.cpp b/src/main_vendor.cpp deleted file mode 100644 index 8dd1f00..0000000 --- a/src/main_vendor.cpp +++ /dev/null @@ -1,187 +0,0 @@ -// Vendor-baseline + custom-shim diagnostic for the JC3248W535 AXS15231B. -// -// Uses the MANUFACTURER'S ESP-IDF panel driver (src/vendor/esp_lcd_axs15231b.c) -// via a CUSTOM esp_lcd_panel_io_t shim (src/vendor/my_panel_io.c) that handles -// the QSPI wire framing. This side-steps the missing `quad_mode` flag in stock -// arduino-esp32 3.0.17 while keeping all vendor-sequencing logic intact. -// -// Guarded so that only the jc3248w535_vendor env compiles this TU. - -#ifdef BOARD_IS_JC3248W535_VENDOR - -#include -#include -#include -#include -#include -#include -#include -#include - -extern "C" { -#include "vendor/esp_lcd_axs15231b.h" -#include "vendor/my_panel_io.h" -} - -#define PIN_QSPI_CS 45 -#define PIN_QSPI_SCK 47 -#define PIN_QSPI_D0 21 -#define PIN_QSPI_D1 48 -#define PIN_QSPI_D2 40 -#define PIN_QSPI_D3 39 -#define PIN_BL 1 -#define PIN_SDA 4 -#define PIN_SCL 8 - -#define LCD_W 320 -#define LCD_H 480 - -static spi_device_handle_t g_spi = nullptr; -static esp_lcd_panel_io_handle_t g_io = nullptr; -static esp_lcd_panel_handle_t g_panel = nullptr; - -// Vendor BSP override init table (from esp_bsp.c lines 34-67). -static const axs15231b_lcd_init_cmd_t kInitCmds[] = { - {0xBB, (uint8_t[]){0x00,0x00,0x00,0x00,0x00,0x00,0x5A,0xA5}, 8, 0}, - {0xA0, (uint8_t[]){0xC0,0x10,0x00,0x02,0x00,0x00,0x04,0x3F,0x20,0x05,0x3F,0x3F,0x00,0x00,0x00,0x00,0x00}, 17, 0}, - {0xA2, (uint8_t[]){0x30,0x3C,0x24,0x14,0xD0,0x20,0xFF,0xE0,0x40,0x19,0x80,0x80,0x80,0x20,0xF9,0x10,0x02,0xFF,0xFF,0xF0,0x90,0x01,0x32,0xA0,0x91,0xE0,0x20,0x7F,0xFF,0x00,0x5A}, 31, 0}, - {0xD0, (uint8_t[]){0xE0,0x40,0x51,0x24,0x08,0x05,0x10,0x01,0x20,0x15,0x42,0xC2,0x22,0x22,0xAA,0x03,0x10,0x12,0x60,0x14,0x1E,0x51,0x15,0x00,0x8A,0x20,0x00,0x03,0x3A,0x12}, 30, 0}, - {0xA3, (uint8_t[]){0xA0,0x06,0xAA,0x00,0x08,0x02,0x0A,0x04,0x04,0x04,0x04,0x04,0x04,0x04,0x04,0x04,0x04,0x04,0x04,0x00,0x55,0x55}, 22, 0}, - {0xC1, (uint8_t[]){0x31,0x04,0x02,0x02,0x71,0x05,0x24,0x55,0x02,0x00,0x41,0x00,0x53,0xFF,0xFF,0xFF,0x4F,0x52,0x00,0x4F,0x52,0x00,0x45,0x3B,0x0B,0x02,0x0D,0x00,0xFF,0x40}, 30, 0}, - {0xC3, (uint8_t[]){0x00,0x00,0x00,0x50,0x03,0x00,0x00,0x00,0x01,0x80,0x01}, 11, 0}, - {0xC4, (uint8_t[]){0x00,0x24,0x33,0x80,0x00,0xEA,0x64,0x32,0xC8,0x64,0xC8,0x32,0x90,0x90,0x11,0x06,0xDC,0xFA,0x00,0x00,0x80,0xFE,0x10,0x10,0x00,0x0A,0x0A,0x44,0x50}, 29, 0}, - {0xC5, (uint8_t[]){0x18,0x00,0x00,0x03,0xFE,0x3A,0x4A,0x20,0x30,0x10,0x88,0xDE,0x0D,0x08,0x0F,0x0F,0x01,0x3A,0x4A,0x20,0x10,0x10,0x00}, 23, 0}, - {0xC6, (uint8_t[]){0x05,0x0A,0x05,0x0A,0x00,0xE0,0x2E,0x0B,0x12,0x22,0x12,0x22,0x01,0x03,0x00,0x3F,0x6A,0x18,0xC8,0x22}, 20, 0}, - {0xC7, (uint8_t[]){0x50,0x32,0x28,0x00,0xA2,0x80,0x8F,0x00,0x80,0xFF,0x07,0x11,0x9C,0x67,0xFF,0x24,0x0C,0x0D,0x0E,0x0F}, 20, 0}, - {0xC9, (uint8_t[]){0x33,0x44,0x44,0x01}, 4, 0}, - {0xCF, (uint8_t[]){0x2C,0x1E,0x88,0x58,0x13,0x18,0x56,0x18,0x1E,0x68,0x88,0x00,0x65,0x09,0x22,0xC4,0x0C,0x77,0x22,0x44,0xAA,0x55,0x08,0x08,0x12,0xA0,0x08}, 27, 0}, - {0xD5, (uint8_t[]){0x40,0x8E,0x8D,0x01,0x35,0x04,0x92,0x74,0x04,0x92,0x74,0x04,0x08,0x6A,0x04,0x46,0x03,0x03,0x03,0x03,0x82,0x01,0x03,0x00,0xE0,0x51,0xA1,0x00,0x00,0x00}, 30, 0}, - {0xD6, (uint8_t[]){0x10,0x32,0x54,0x76,0x98,0xBA,0xDC,0xFE,0x93,0x00,0x01,0x83,0x07,0x07,0x00,0x07,0x07,0x00,0x03,0x03,0x03,0x03,0x03,0x03,0x00,0x84,0x00,0x20,0x01,0x00}, 30, 0}, - {0xD7, (uint8_t[]){0x03,0x01,0x0B,0x09,0x0F,0x0D,0x1E,0x1F,0x18,0x1D,0x1F,0x19,0x40,0x8E,0x04,0x00,0x20,0xA0,0x1F}, 19, 0}, - {0xD8, (uint8_t[]){0x02,0x00,0x0A,0x08,0x0E,0x0C,0x1E,0x1F,0x18,0x1D,0x1F,0x19}, 12, 0}, - {0xD9, (uint8_t[]){0x1F,0x1F,0x1F,0x1F,0x1F,0x1F,0x1F,0x1F,0x1F,0x1F,0x1F,0x1F}, 12, 0}, - {0xDD, (uint8_t[]){0x1F,0x1F,0x1F,0x1F,0x1F,0x1F,0x1F,0x1F,0x1F,0x1F,0x1F,0x1F}, 12, 0}, - {0xDF, (uint8_t[]){0x44,0x73,0x4B,0x69,0x00,0x0A,0x02,0x90}, 8, 0}, - {0xE0, (uint8_t[]){0x3B,0x28,0x10,0x16,0x0C,0x06,0x11,0x28,0x5C,0x21,0x0D,0x35,0x13,0x2C,0x33,0x28,0x0D}, 17, 0}, - {0xE1, (uint8_t[]){0x37,0x28,0x10,0x16,0x0B,0x06,0x11,0x28,0x5C,0x21,0x0D,0x35,0x14,0x2C,0x33,0x28,0x0F}, 17, 0}, - {0xE2, (uint8_t[]){0x3B,0x07,0x12,0x18,0x0E,0x0D,0x17,0x35,0x44,0x32,0x0C,0x14,0x14,0x36,0x3A,0x2F,0x0D}, 17, 0}, - {0xE3, (uint8_t[]){0x37,0x07,0x12,0x18,0x0E,0x0D,0x17,0x35,0x44,0x32,0x0C,0x14,0x14,0x36,0x32,0x2F,0x0F}, 17, 0}, - {0xE4, (uint8_t[]){0x3B,0x07,0x12,0x18,0x0E,0x0D,0x17,0x39,0x44,0x2E,0x0C,0x14,0x14,0x36,0x3A,0x2F,0x0D}, 17, 0}, - {0xE5, (uint8_t[]){0x37,0x07,0x12,0x18,0x0E,0x0D,0x17,0x39,0x44,0x2E,0x0C,0x14,0x14,0x36,0x3A,0x2F,0x0F}, 17, 0}, - {0xA4, (uint8_t[]){0x85,0x85,0x95,0x82,0xAF,0xAA,0xAA,0x80,0x10,0x30,0x40,0x40,0x20,0xFF,0x60,0x30}, 16, 0}, - {0xA4, (uint8_t[]){0x85,0x85,0x95,0x85}, 4, 0}, - {0xBB, (uint8_t[]){0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, 8, 0}, - {0x13, (uint8_t[]){0x00}, 0, 0}, - {0x11, (uint8_t[]){0x00}, 0, 120}, - {0x2C, (uint8_t[]){0x00,0x00,0x00,0x00}, 4, 0}, -}; - -static void backlight_init(uint8_t pct) { - ledc_timer_config_t t = {}; - t.speed_mode = LEDC_LOW_SPEED_MODE; t.duty_resolution = LEDC_TIMER_10_BIT; - t.timer_num = LEDC_TIMER_1; t.freq_hz = 5000; t.clk_cfg = LEDC_AUTO_CLK; - ledc_timer_config(&t); - ledc_channel_config_t c = {}; - c.gpio_num = PIN_BL; c.speed_mode = LEDC_LOW_SPEED_MODE; - c.channel = LEDC_CHANNEL_1; c.timer_sel = LEDC_TIMER_1; - c.duty = (1023u * pct) / 100u; c.hpoint = 0; - ledc_channel_config(&c); -} - -static void paint(uint16_t c565) { - const size_t N = (size_t)LCD_W * LCD_H; - uint16_t* buf = (uint16_t*)ps_malloc(N * sizeof(uint16_t)); - if (!buf) { Serial.println("alloc fail"); return; } - uint16_t sw = __builtin_bswap16(c565); - for (size_t i = 0; i < N; ++i) buf[i] = sw; - esp_lcd_panel_draw_bitmap(g_panel, 0, 0, LCD_W, LCD_H, buf); - free(buf); -} - -void setup() { - Serial.begin(115200); - delay(300); - Serial.println("\n=== VENDOR DRIVER + CUSTOM SHIM ==="); - - backlight_init(100); - Wire.begin(PIN_SDA, PIN_SCL); - Wire.setClock(400000); - - // SPI bus. CS is driven by the peripheral (not GPIO) so our shim can use - // SPI_TRANS_CS_KEEP_ACTIVE to hold CS across the 32-bit cmd + data chunks, - // matching stock esp_lcd_panel_io_spi behavior. - spi_bus_config_t bcfg = {}; - bcfg.mosi_io_num = PIN_QSPI_D0; - bcfg.miso_io_num = PIN_QSPI_D1; - bcfg.sclk_io_num = PIN_QSPI_SCK; - bcfg.quadwp_io_num = PIN_QSPI_D2; - bcfg.quadhd_io_num = PIN_QSPI_D3; - bcfg.max_transfer_sz = 4096; - bcfg.flags = SPICOMMON_BUSFLAG_MASTER | SPICOMMON_BUSFLAG_GPIO_PINS - | SPICOMMON_BUSFLAG_QUAD; // force D2/D3 to be driven as outputs - esp_err_t r = spi_bus_initialize(SPI2_HOST, &bcfg, SPI_DMA_CH_AUTO); - Serial.printf("[bsp] spi_bus_initialize -> %d\n", r); - - spi_device_interface_config_t dcfg = {}; - dcfg.command_bits = 0; - dcfg.address_bits = 0; - dcfg.mode = 3; - dcfg.clock_speed_hz = 40 * 1000 * 1000; - dcfg.spics_io_num = PIN_QSPI_CS; // peripheral-driven CS for KEEP_ACTIVE - dcfg.queue_size = 10; // stock esp_lcd uses 10 - dcfg.flags = SPI_DEVICE_HALFDUPLEX; - r = spi_bus_add_device(SPI2_HOST, &dcfg, &g_spi); - Serial.printf("[bsp] spi_bus_add_device -> %d\n", r); - - // Create our custom panel_io, then hand it to the vendor panel driver. - r = my_panel_io_new(g_spi, PIN_QSPI_CS, &g_io); - Serial.printf("[bsp] my_panel_io_new -> %d\n", r); - - axs15231b_vendor_config_t vcfg = {}; - // Let the panel driver use its built-in `vendor_specific_init_default` - // table (passing NULL init_cmds). That's the "type 1" 320x480 init which - // ends with DISPON + ALLPOFF(0x22). The esp_bsp.c override table we used - // before ends without ALLPOFF which may leave the chip in an indeterminate - // DDRAM-display state producing our noise symptom. - vcfg.init_cmds = NULL; - vcfg.init_cmds_size = 0; - vcfg.flags.use_qspi_interface = 1; - esp_lcd_panel_dev_config_t pcfg = {}; - pcfg.reset_gpio_num = -1; - pcfg.color_space = LCD_RGB_ELEMENT_ORDER_RGB; - pcfg.bits_per_pixel = 16; - pcfg.vendor_config = &vcfg; - r = esp_lcd_new_panel_axs15231b(g_io, &pcfg, &g_panel); - Serial.printf("[bsp] new_panel_axs15231b -> %d\n", r); - - esp_lcd_panel_reset(g_panel); - esp_lcd_panel_init(g_panel); - esp_lcd_panel_disp_on_off(g_panel, true); - Serial.println("[bsp] panel on"); - - const struct { uint16_t c; const char* n; } seq[] = { - {0xF800,"RED"},{0x07E0,"GREEN"},{0x001F,"BLUE"}, - {0xFFFF,"WHITE"},{0x0000,"BLACK"}, - }; - for (auto& s : seq) { Serial.printf("paint %s\n", s.n); paint(s.c); delay(800); } - Serial.println("halted"); -} - -static bool touch_down() { - static const uint8_t cmd[11] = { 0xB5,0xAB,0xA5,0x5A, 0x00,0x00, 0x00,0x08, 0x00,0x00,0x00 }; - Wire.beginTransmission(0x3B); - Wire.write(cmd, sizeof(cmd)); - if (Wire.endTransmission() != 0) return false; - uint8_t b[8] = {0}; - size_t got = Wire.requestFrom((uint8_t)0x3B, (uint8_t)8); - if (got < 6) return false; - for (size_t i = 0; i < got && i < sizeof(b); ++i) b[i] = Wire.read(); - return (b[0] == 0) && ((b[1] & 0x0F) != 0); -} - -void loop() { - if (touch_down()) { Serial.println("touch → restart"); delay(50); ESP.restart(); } - delay(30); -} - -#endif // BOARD_IS_JC3248W535_VENDOR diff --git a/src/skeleton_test.cpp b/src/skeleton_test.cpp deleted file mode 100644 index d7f3f97..0000000 --- a/src/skeleton_test.cpp +++ /dev/null @@ -1,57 +0,0 @@ -// Clean-slate JC3248W535 diagnostic using the moononournation/Arduino_GFX -// library's built-in Arduino_AXS15231B panel class and Arduino_ESP32QSPI -// databus. Per jc3248w535 skill lines 623-640, this is "production-grade" -// on this chip and requires no driver code. -// -// If this binary paints solid colors correctly, the hardware + toolchain are -// proven good and every previous symptom we chased was a bug in our driver. -// If it doesn't, something else is going on with this particular board. -// -// Pin map from skill (verified against vendor pincfg.h): -// CS=45, SCK=47, D0=21, D1=48, D2=40, D3=39, BL=1 -// -// Guarded so only the jc3248w535_skel env compiles this translation unit. -#ifdef BOARD_IS_JC3248W535_SKEL - -#include -#include - -static Arduino_DataBus *bus = new Arduino_ESP32QSPI( - 45 /*CS*/, 47 /*SCK*/, 21 /*D0*/, 48 /*D1*/, 40 /*D2*/, 39 /*D3*/); - -// IPS=true sends INVON (0x21) during init, which double-inverts this panel -// (observed: every color appeared as its bitwise complement). Setting IPS=false -// skips INVON so colors display correctly. -static Arduino_GFX *gfx = new Arduino_AXS15231B( - bus, -1 /*RST*/, 0 /*rotation*/, false /*IPS*/, 320, 480); - -void setup() { - Serial.begin(115200); - delay(300); - Serial.println("\n=== AXS15231B Arduino_GFX baseline ==="); - - // Backlight on (LEDC not strictly needed; simple GPIO-high works for test) - pinMode(1, OUTPUT); - digitalWrite(1, HIGH); - - // 32 MHz pclk per skill's recommended starting point (skill line 42). - if (!gfx->begin(32000000UL)) { - Serial.println("gfx->begin() FAILED"); - return; - } - Serial.println("gfx begin OK"); - - gfx->fillScreen(RED); Serial.println("RED"); delay(1500); - gfx->fillScreen(GREEN); Serial.println("GREEN"); delay(1500); - gfx->fillScreen(BLUE); Serial.println("BLUE"); delay(1500); - gfx->fillScreen(WHITE); Serial.println("WHITE"); delay(1500); - gfx->fillScreen(BLACK); Serial.println("BLACK"); delay(1500); - - Serial.println("Diagnostic halted"); -} - -void loop() { - delay(1000); -} - -#endif // BOARD_IS_JC3248W535_SKEL diff --git a/src/vendor/esp_lcd_axs15231b.c b/src/vendor/esp_lcd_axs15231b.c deleted file mode 100644 index edf304d..0000000 --- a/src/vendor/esp_lcd_axs15231b.c +++ /dev/null @@ -1,392 +0,0 @@ -#ifdef BOARD_IS_JC3248W535_VENDOR -/* - * SPDX-FileCopyrightText: 2021-2024 Espressif Systems (Shanghai) CO LTD - * - * SPDX-License-Identifier: Apache-2.0 - */ - -#include -#include -#include "freertos/FreeRTOS.h" -#include "freertos/task.h" -#include "esp_lcd_panel_interface.h" -#include "esp_lcd_panel_io.h" -#include "esp_lcd_panel_vendor.h" -#include "esp_lcd_panel_ops.h" -#include "esp_lcd_panel_commands.h" -#include "driver/gpio.h" -#include "esp_log.h" -#include "esp_check.h" -#include "esp_lcd_touch.h" - -#include "esp_lcd_axs15231b.h" - -/*max point num*/ -#define AXS_MAX_TOUCH_NUMBER (1) - -#define LCD_OPCODE_WRITE_CMD (0x02ULL) -#define LCD_OPCODE_READ_CMD (0x0BULL) -#define LCD_OPCODE_WRITE_COLOR (0x32ULL) - -static const char *TAG = "lcd_panel.axs15231b"; - -static esp_err_t panel_axs15231b_del(esp_lcd_panel_t *panel); -static esp_err_t panel_axs15231b_reset(esp_lcd_panel_t *panel); -static esp_err_t panel_axs15231b_init(esp_lcd_panel_t *panel); -static esp_err_t panel_axs15231b_draw_bitmap(esp_lcd_panel_t *panel, int x_start, int y_start, int x_end, int y_end, const void *color_data); -static esp_err_t panel_axs15231b_invert_color(esp_lcd_panel_t *panel, bool invert_color_data); -static esp_err_t panel_axs15231b_mirror(esp_lcd_panel_t *panel, bool mirror_x, bool mirror_y); -static esp_err_t panel_axs15231b_swap_xy(esp_lcd_panel_t *panel, bool swap_axes); -static esp_err_t panel_axs15231b_set_gap(esp_lcd_panel_t *panel, int x_gap, int y_gap); -static esp_err_t panel_axs15231b_disp_off(esp_lcd_panel_t *panel, bool off); - -// Touch forward declarations and implementation removed — this build is -// display-only. Touch is driven by a separate I2C path in main_vendor.cpp. - -typedef struct { - esp_lcd_panel_t base; - esp_lcd_panel_io_handle_t io; - int reset_gpio_num; - int x_gap; - int y_gap; - uint8_t fb_bits_per_pixel; - uint8_t madctl_val; // save current value of LCD_CMD_MADCTL register - uint8_t colmod_val; // save surrent value of LCD_CMD_COLMOD register - const axs15231b_lcd_init_cmd_t *init_cmds; - uint16_t init_cmds_size; - struct { - unsigned int use_qspi_interface: 1; - unsigned int reset_level: 1; - } flags; -} axs15231b_panel_t; - -esp_err_t esp_lcd_new_panel_axs15231b(const esp_lcd_panel_io_handle_t io, const esp_lcd_panel_dev_config_t *panel_dev_config, esp_lcd_panel_handle_t *ret_panel) -{ - esp_err_t ret = ESP_OK; - axs15231b_panel_t *axs15231b = NULL; - ESP_GOTO_ON_FALSE(io && panel_dev_config && ret_panel, ESP_ERR_INVALID_ARG, err, TAG, "invalid argument"); - axs15231b = calloc(1, sizeof(axs15231b_panel_t)); - ESP_GOTO_ON_FALSE(axs15231b, ESP_ERR_NO_MEM, err, TAG, "no mem for axs15231b panel"); - - if (panel_dev_config->reset_gpio_num >= 0) { - gpio_config_t io_conf = { - .mode = GPIO_MODE_OUTPUT, - .pin_bit_mask = 1ULL << panel_dev_config->reset_gpio_num, - }; - ESP_GOTO_ON_ERROR(gpio_config(&io_conf), err, TAG, "configure GPIO for RST line failed"); - } - - switch (panel_dev_config->color_space) { - case LCD_RGB_ELEMENT_ORDER_RGB: - axs15231b->madctl_val = 0; - break; - case LCD_RGB_ELEMENT_ORDER_BGR: - axs15231b->madctl_val |= LCD_CMD_BGR_BIT; - break; - default: - ESP_GOTO_ON_FALSE(false, ESP_ERR_NOT_SUPPORTED, err, TAG, "unsupported RGB element order"); - break; - } - - uint8_t fb_bits_per_pixel = 0; - switch (panel_dev_config->bits_per_pixel) { - case 16: // RGB565 - axs15231b->colmod_val = 0x55; - fb_bits_per_pixel = 16; - break; - case 18: // RGB666 - axs15231b->colmod_val = 0x66; - // each color component (R/G/B) should occupy the 6 high bits of a byte, which means 3 full bytes are required for a pixel - fb_bits_per_pixel = 24; - break; - default: - ESP_GOTO_ON_FALSE(false, ESP_ERR_NOT_SUPPORTED, err, TAG, "unsupported pixel width"); - break; - } - - axs15231b->io = io; - axs15231b->fb_bits_per_pixel = fb_bits_per_pixel; - axs15231b->reset_gpio_num = panel_dev_config->reset_gpio_num; - axs15231b->flags.reset_level = panel_dev_config->flags.reset_active_high; - if (panel_dev_config->vendor_config) { - axs15231b->init_cmds = ((axs15231b_vendor_config_t *)panel_dev_config->vendor_config)->init_cmds; - axs15231b->init_cmds_size = ((axs15231b_vendor_config_t *)panel_dev_config->vendor_config)->init_cmds_size; - axs15231b->flags.use_qspi_interface = ((axs15231b_vendor_config_t *)panel_dev_config->vendor_config)->flags.use_qspi_interface; - } - axs15231b->base.del = panel_axs15231b_del; - axs15231b->base.reset = panel_axs15231b_reset; - axs15231b->base.init = panel_axs15231b_init; - axs15231b->base.draw_bitmap = panel_axs15231b_draw_bitmap; - axs15231b->base.invert_color = panel_axs15231b_invert_color; - axs15231b->base.set_gap = panel_axs15231b_set_gap; - axs15231b->base.mirror = panel_axs15231b_mirror; - axs15231b->base.swap_xy = panel_axs15231b_swap_xy; - axs15231b->base.disp_off = panel_axs15231b_disp_off; // arduino-esp32 3.0.x/ESP-IDF 5.1 struct field name - *ret_panel = &(axs15231b->base); - ESP_LOGD(TAG, "new axs15231b panel @%p", axs15231b); - ESP_LOGI(TAG, "LCD panel create success, version: %d.%d.%d", ESP_LCD_AXS15231B_VER_MAJOR, ESP_LCD_AXS15231B_VER_MINOR, - ESP_LCD_AXS15231B_VER_PATCH); - - return ESP_OK; - -err: - if (axs15231b) { - if (panel_dev_config->reset_gpio_num >= 0) { - gpio_reset_pin(panel_dev_config->reset_gpio_num); - } - free(axs15231b); - } - return ret; -} - -static esp_err_t tx_param(axs15231b_panel_t *axs15231b, esp_lcd_panel_io_handle_t io, int lcd_cmd, const void *param, size_t param_size) -{ - if (axs15231b->flags.use_qspi_interface) { - lcd_cmd &= 0xff; - lcd_cmd <<= 8; - lcd_cmd |= LCD_OPCODE_WRITE_CMD << 24; - } - return esp_lcd_panel_io_tx_param(io, lcd_cmd, param, param_size); -} - -static esp_err_t tx_color(axs15231b_panel_t *axs15231b, esp_lcd_panel_io_handle_t io, int lcd_cmd, const void *param, size_t param_size) -{ - if (axs15231b->flags.use_qspi_interface) { - lcd_cmd &= 0xff; - lcd_cmd <<= 8; - lcd_cmd |= LCD_OPCODE_WRITE_COLOR << 24; - } - return esp_lcd_panel_io_tx_color(io, lcd_cmd, param, param_size); -} - -static esp_err_t panel_axs15231b_del(esp_lcd_panel_t *panel) -{ - axs15231b_panel_t *axs15231b = __containerof(panel, axs15231b_panel_t, base); - - if (axs15231b->reset_gpio_num >= 0) { - gpio_reset_pin(axs15231b->reset_gpio_num); - } - ESP_LOGD(TAG, "del axs15231b panel @%p", axs15231b); - free(axs15231b); - return ESP_OK; -} - -static esp_err_t panel_axs15231b_reset(esp_lcd_panel_t *panel) -{ - axs15231b_panel_t *axs15231b = __containerof(panel, axs15231b_panel_t, base); - esp_lcd_panel_io_handle_t io = axs15231b->io; - - // perform hardware reset - if (axs15231b->reset_gpio_num >= 0) { - gpio_set_level(axs15231b->reset_gpio_num, !axs15231b->flags.reset_level); - vTaskDelay(pdMS_TO_TICKS(10)); - gpio_set_level(axs15231b->reset_gpio_num, axs15231b->flags.reset_level); - vTaskDelay(pdMS_TO_TICKS(10)); - gpio_set_level(axs15231b->reset_gpio_num, !axs15231b->flags.reset_level); - vTaskDelay(pdMS_TO_TICKS(120)); - } else { // perform software reset - tx_param(axs15231b, io, LCD_CMD_SWRESET, NULL, 0); - vTaskDelay(pdMS_TO_TICKS(120)); // spec, wait at least 5m before sending new command - } - - return ESP_OK; -} - -static const axs15231b_lcd_init_cmd_t vendor_specific_init_default[] = { - {0xBB, (uint8_t[]){0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5A, 0xA5}, 8, 0}, - {0xA0, (uint8_t[]){0x00, 0x10, 0x00, 0x02, 0x00, 0x00, 0x64, 0x3F, 0x20, 0x05, 0x3F, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00}, 17, 0}, - {0xA2, (uint8_t[]){0x30, 0x04, 0x0A, 0x3C, 0xEC, 0x54, 0xC4, 0x30, 0xAC, 0x28, 0x7F, 0x7F, 0x7F, 0x20, 0xF8, 0x10, 0x02, 0xFF, 0xFF, 0xF0, 0x90, 0x01, 0x32, 0xA0, 0x91, 0xC0, 0x20, 0x7F, 0xFF, 0x00, 0x54}, 31, 0}, - {0xD0, (uint8_t[]){0x30, 0xAC, 0x21, 0x24, 0x08, 0x09, 0x10, 0x01, 0xAA, 0x14, 0xC2, 0x00, 0x22, 0x22, 0xAA, 0x03, 0x10, 0x12, 0x40, 0x14, 0x1E, 0x51, 0x15, 0x00, 0x40, 0x10, 0x00, 0x03, 0x3D, 0x12}, 30, 0}, - {0xA3, (uint8_t[]){0xA0, 0x06, 0xAA, 0x08, 0x08, 0x02, 0x0A, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x00, 0x55, 0x55}, 22, 0}, - {0xC1, (uint8_t[]){0x33, 0x04, 0x02, 0x02, 0x71, 0x05, 0x24, 0x55, 0x02, 0x00, 0x41, 0x00, 0x53, 0xFF, 0xFF, 0xFF, 0x4F, 0x52, 0x00, 0x4F, 0x52, 0x00, 0x45, 0x3B, 0x0B, 0x02, 0x0D, 0x00, 0xFF, 0x40}, 30, 0}, - {0xC3, (uint8_t[]){0x00, 0x00, 0x00, 0x50, 0x03, 0x00, 0x00, 0x00, 0x01, 0x80, 0x01}, 11, 0}, - {0xC4, (uint8_t[]){0x00, 0x24, 0x33, 0x90, 0x50, 0xea, 0x64, 0x32, 0xC8, 0x64, 0xC8, 0x32, 0x90, 0x90, 0x11, 0x06, 0xDC, 0xFA, 0x04, 0x03, 0x80, 0xFE, 0x10, 0x10, 0x00, 0x0A, 0x0A, 0x44, 0x50}, 29, 0}, - {0xC5, (uint8_t[]){0x18, 0x00, 0x00, 0x03, 0xFE, 0x78, 0x33, 0x20, 0x30, 0x10, 0x88, 0xDE, 0x0D, 0x08, 0x0F, 0x0F, 0x01, 0x78, 0x33, 0x20, 0x10, 0x10, 0x80}, 23, 0}, - {0xC6, (uint8_t[]){0x05, 0x0A, 0x05, 0x0A, 0x00, 0xE0, 0x2E, 0x0B, 0x12, 0x22, 0x12, 0x22, 0x01, 0x00, 0x00, 0x3F, 0x6A, 0x18, 0xC8, 0x22}, 20, 0}, - {0xC7, (uint8_t[]){0x50, 0x32, 0x28, 0x00, 0xa2, 0x80, 0x8f, 0x00, 0x80, 0xff, 0x07, 0x11, 0x9F, 0x6f, 0xff, 0x26, 0x0c, 0x0d, 0x0e, 0x0f}, 20, 0}, - {0xC9, (uint8_t[]){0x33, 0x44, 0x44, 0x01}, 4, 0}, - {0xCF, (uint8_t[]){0x34, 0x1E, 0x88, 0x58, 0x13, 0x18, 0x56, 0x18, 0x1E, 0x68, 0xF7, 0x00, 0x65, 0x0C, 0x22, 0xC4, 0x0C, 0x77, 0x22, 0x44, 0xAA, 0x55, 0x04, 0x04, 0x12, 0xA0, 0x08}, 27, 0}, - {0xD5, (uint8_t[]){0x3E, 0x3E, 0x88, 0x00, 0x44, 0x04, 0x78, 0x33, 0x20, 0x78, 0x33, 0x20, 0x04, 0x28, 0xD3, 0x47, 0x03, 0x03, 0x03, 0x03, 0x86, 0x00, 0x00, 0x00, 0x30, 0x52, 0x3f, 0x40, 0x40, 0x96}, 30, 0}, - {0xD6, (uint8_t[]){0x10, 0x32, 0x54, 0x76, 0x98, 0xBA, 0xDC, 0xFE, 0x95, 0x00, 0x01, 0x83, 0x75, 0x36, 0x20, 0x75, 0x36, 0x20, 0x3F, 0x03, 0x03, 0x03, 0x10, 0x10, 0x00, 0x04, 0x51, 0x20, 0x01, 0x00}, 30, 0}, - {0xD7, (uint8_t[]){0x0a, 0x08, 0x0e, 0x0c, 0x1E, 0x18, 0x19, 0x1F, 0x00, 0x1F, 0x1A, 0x1F, 0x3E, 0x3E, 0x04, 0x00, 0x1F, 0x1F, 0x1F}, 19, 0}, - {0xD8, (uint8_t[]){0x0B, 0x09, 0x0F, 0x0D, 0x1E, 0x18, 0x19, 0x1F, 0x01, 0x1F, 0x1A, 0x1F}, 12, 0}, - {0xD9, (uint8_t[]){0x00, 0x0D, 0x0F, 0x09, 0x0B, 0x1F, 0x18, 0x19, 0x1F, 0x01, 0x1E, 0x1A, 0x1F}, 13, 0}, - {0xDD, (uint8_t[]){0x0C, 0x0E, 0x08, 0x0A, 0x1F, 0x18, 0x19, 0x1F, 0x00, 0x1E, 0x1A, 0x1F}, 12, 0}, - {0xDF, (uint8_t[]){0x44, 0x73, 0x4B, 0x69, 0x00, 0x0A, 0x02, 0x90}, 8, 0}, - {0xE0, (uint8_t[]){0x19, 0x20, 0x0A, 0x13, 0x0E, 0x09, 0x12, 0x28, 0xD4, 0x24, 0x0C, 0x35, 0x13, 0x31, 0x36, 0x2f, 0x03}, 17, 0}, - {0xE1, (uint8_t[]){0x38, 0x20, 0x09, 0x12, 0x0E, 0x08, 0x12, 0x28, 0xC5, 0x24, 0x0C, 0x34, 0x12, 0x31, 0x36, 0x2f, 0x27}, 17, 0}, - {0xE2, (uint8_t[]){0x19, 0x20, 0x0A, 0x11, 0x09, 0x06, 0x11, 0x25, 0xD4, 0x22, 0x0B, 0x33, 0x12, 0x2D, 0x32, 0x2f, 0x03}, 17, 0}, - {0xE3, (uint8_t[]){0x38, 0x20, 0x0A, 0x11, 0x09, 0x06, 0x11, 0x25, 0xC4, 0x21, 0x0A, 0x32, 0x11, 0x2C, 0x32, 0x2f, 0x27}, 17, 0}, - {0xE4, (uint8_t[]){0x19, 0x20, 0x0D, 0x14, 0x0D, 0x08, 0x12, 0x2A, 0xD4, 0x26, 0x0E, 0x35, 0x13, 0x34, 0x39, 0x2f, 0x03}, 17, 0}, - {0xE5, (uint8_t[]){0x38, 0x20, 0x0D, 0x13, 0x0D, 0x07, 0x12, 0x29, 0xC4, 0x25, 0x0D, 0x35, 0x12, 0x33, 0x39, 0x2f, 0x27}, 17, 0}, - {0xBB, (uint8_t[]){0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, 8, 0}, - {0x13, (uint8_t[]){0x00}, 0, 0}, - {0x11, (uint8_t[]){0x00}, 0, 200}, - {0x29, (uint8_t[]){0x00}, 0, 200}, - {0x2C, (uint8_t[]){0x00, 0x00, 0x00, 0x00}, 4, 0}, - {0x22, (uint8_t[]){0x00}, 0, 200},//All Pixels off -}; - -static esp_err_t panel_axs15231b_init(esp_lcd_panel_t *panel) -{ - axs15231b_panel_t *axs15231b = __containerof(panel, axs15231b_panel_t, base); - esp_lcd_panel_io_handle_t io = axs15231b->io; - - // LCD goes into sleep mode and display will be turned off after power on reset, exit sleep mode first - ESP_RETURN_ON_ERROR(tx_param(axs15231b, io, LCD_CMD_SLPOUT, NULL, 0), TAG, "send command failed"); - vTaskDelay(pdMS_TO_TICKS(100)); - ESP_RETURN_ON_ERROR(tx_param(axs15231b, io, LCD_CMD_MADCTL, (uint8_t[]) { - axs15231b->madctl_val, - }, 1), TAG, "send command failed"); - ESP_RETURN_ON_ERROR(tx_param(axs15231b, io, LCD_CMD_COLMOD, (uint8_t[]) { - axs15231b->colmod_val, - }, 1), TAG, "send command failed"); - - const axs15231b_lcd_init_cmd_t *init_cmds = NULL; - uint16_t init_cmds_size = 0; - if (axs15231b->init_cmds) { - init_cmds = axs15231b->init_cmds; - init_cmds_size = axs15231b->init_cmds_size; - } else { - init_cmds = vendor_specific_init_default; - init_cmds_size = sizeof(vendor_specific_init_default) / sizeof(axs15231b_lcd_init_cmd_t); - } - - bool is_cmd_overwritten = false; - for (int i = 0; i < init_cmds_size; i++) { - // Check if the command has been used or conflicts with the internal - switch (init_cmds[i].cmd) { - case LCD_CMD_MADCTL: - is_cmd_overwritten = true; - axs15231b->madctl_val = ((uint8_t *)init_cmds[i].data)[0]; - break; - case LCD_CMD_COLMOD: - is_cmd_overwritten = true; - axs15231b->colmod_val = ((uint8_t *)init_cmds[i].data)[0]; - break; - default: - is_cmd_overwritten = false; - break; - } - - if (is_cmd_overwritten) { - ESP_LOGW(TAG, "The %02Xh command has been used and will be overwritten by external initialization sequence", init_cmds[i].cmd); - } - ESP_RETURN_ON_ERROR(tx_param(axs15231b, io, init_cmds[i].cmd, init_cmds[i].data, init_cmds[i].data_bytes), TAG, "send command failed"); - vTaskDelay(pdMS_TO_TICKS(init_cmds[i].delay_ms)); - } - ESP_LOGI(TAG, "send init commands success"); - - return ESP_OK; -} - -static esp_err_t panel_axs15231b_draw_bitmap(esp_lcd_panel_t *panel, int x_start, int y_start, int x_end, int y_end, const void *color_data) -{ - axs15231b_panel_t *axs15231b = __containerof(panel, axs15231b_panel_t, base); - assert((x_start < x_end) && (y_start < y_end) && "start position must be smaller than end position"); - esp_lcd_panel_io_handle_t io = axs15231b->io; - - x_start += axs15231b->x_gap; - x_end += axs15231b->x_gap; - y_start += axs15231b->y_gap; - y_end += axs15231b->y_gap; - - // define an area of frame memory where MCU can access - tx_param(axs15231b, io, LCD_CMD_CASET, (uint8_t[]) { - (x_start >> 8) & 0xFF, - x_start & 0xFF, - ((x_end - 1) >> 8) & 0xFF, - (x_end - 1) & 0xFF, - }, 4); - - if (0 == axs15231b->flags.use_qspi_interface) { - tx_param(axs15231b, io, LCD_CMD_RASET, (uint8_t[]) { - (y_start >> 8) & 0xFF, - y_start & 0xFF, - ((y_end - 1) >> 8) & 0xFF, - (y_end - 1) & 0xFF, - }, 4); - } - - // transfer frame buffer - size_t len = (x_end - x_start) * (y_end - y_start) * axs15231b->fb_bits_per_pixel / 8; - if (y_start == 0) { - tx_color(axs15231b, io, LCD_CMD_RAMWR, color_data, len);//2C - } else { - tx_color(axs15231b, io, LCD_CMD_RAMWRC, color_data, len);//3C - } - - return ESP_OK; -} - -static esp_err_t panel_axs15231b_invert_color(esp_lcd_panel_t *panel, bool invert_color_data) -{ - axs15231b_panel_t *axs15231b = __containerof(panel, axs15231b_panel_t, base); - esp_lcd_panel_io_handle_t io = axs15231b->io; - int command = 0; - if (invert_color_data) { - command = LCD_CMD_INVON; - } else { - command = LCD_CMD_INVOFF; - } - tx_param(axs15231b, io, command, NULL, 0); - return ESP_OK; -} - -static esp_err_t panel_axs15231b_mirror(esp_lcd_panel_t *panel, bool mirror_x, bool mirror_y) -{ - axs15231b_panel_t *axs15231b = __containerof(panel, axs15231b_panel_t, base); - esp_lcd_panel_io_handle_t io = axs15231b->io; - if (mirror_x) { - axs15231b->madctl_val |= LCD_CMD_MX_BIT; - } else { - axs15231b->madctl_val &= ~LCD_CMD_MX_BIT; - } - if (mirror_y) { - axs15231b->madctl_val |= LCD_CMD_MY_BIT; - } else { - axs15231b->madctl_val &= ~LCD_CMD_MY_BIT; - } - tx_param(axs15231b, io, LCD_CMD_MADCTL, (uint8_t[]) { - axs15231b->madctl_val - }, 1); - return ESP_OK; -} - -static esp_err_t panel_axs15231b_swap_xy(esp_lcd_panel_t *panel, bool swap_axes) -{ - axs15231b_panel_t *axs15231b = __containerof(panel, axs15231b_panel_t, base); - esp_lcd_panel_io_handle_t io = axs15231b->io; - if (swap_axes) { - axs15231b->madctl_val |= LCD_CMD_MV_BIT; - } else { - axs15231b->madctl_val &= ~LCD_CMD_MV_BIT; - } - tx_param(axs15231b, io, LCD_CMD_MADCTL, (uint8_t[]) { - axs15231b->madctl_val - }, 1); - return ESP_OK; -} - -static esp_err_t panel_axs15231b_set_gap(esp_lcd_panel_t *panel, int x_gap, int y_gap) -{ - axs15231b_panel_t *axs15231b = __containerof(panel, axs15231b_panel_t, base); - axs15231b->x_gap = x_gap; - axs15231b->y_gap = y_gap; - return ESP_OK; -} - -static esp_err_t panel_axs15231b_disp_off(esp_lcd_panel_t *panel, bool off) -{ - axs15231b_panel_t *axs15231b = __containerof(panel, axs15231b_panel_t, base); - esp_lcd_panel_io_handle_t io = axs15231b->io; - int command = 0; - if (off) { - command = LCD_CMD_DISPOFF; - } else { - command = LCD_CMD_DISPON; - } - tx_param(axs15231b, io, command, NULL, 0); - return ESP_OK; -} -#endif // BOARD_IS_JC3248W535_VENDOR diff --git a/src/vendor/esp_lcd_axs15231b.h b/src/vendor/esp_lcd_axs15231b.h deleted file mode 100644 index 4f480de..0000000 --- a/src/vendor/esp_lcd_axs15231b.h +++ /dev/null @@ -1,208 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2022-2024 Espressif Systems (Shanghai) CO LTD - * - * SPDX-License-Identifier: Apache-2.0 - */ -/** - * @file - * @brief ESP LCD & Touch: AXS15231B - */ - -#pragma once - -// hal/spi_ll.h removed — not used in this header, and including it from C++ -// breaks due to duplicate operator<<= definitions across multiple FLAG_ATTR -// typedefs in arduino-esp32 3.0.x. -// esp_lcd_touch.h removed — touch API is out of scope for our display-only fork. -#include "esp_lcd_panel_vendor.h" - -// Arduino-esp32 3.0.x ships ESP-IDF 5.1 which uses esp_lcd_color_space_t -// (ESP_LCD_COLOR_SPACE_RGB/BGR). The vendor driver source was written -// against ESP-IDF 5.2+ which renamed these to LCD_RGB_ELEMENT_ORDER_RGB/BGR. -// Define compatibility aliases. -#ifndef LCD_RGB_ELEMENT_ORDER_RGB -#define LCD_RGB_ELEMENT_ORDER_RGB ESP_LCD_COLOR_SPACE_RGB -#define LCD_RGB_ELEMENT_ORDER_BGR ESP_LCD_COLOR_SPACE_BGR -#endif - -#define ESP_LCD_AXS15231B_VER_MAJOR (1) -#define ESP_LCD_AXS15231B_VER_MINOR (0) -#define ESP_LCD_AXS15231B_VER_PATCH (0) - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @brief LCD panel initialization commands. - * - */ -typedef struct { - int cmd; /* -#include "sdkconfig.h" -#include "esp_err.h" -#include "driver/gpio.h" -#include "esp_lcd_panel_io.h" -#include "freertos/FreeRTOS.h" -#include "freertos/semphr.h" - -#ifdef __cplusplus -extern "C" { -#endif - -#define ESP_LCD_TOUCH_VER_MAJOR (1) -#define ESP_LCD_TOUCH_VER_MINOR (1) -#define ESP_LCD_TOUCH_VER_PATCH (2) - -#define CONFIG_ESP_LCD_TOUCH_MAX_POINTS (1) -#define CONFIG_ESP_LCD_TOUCH_MAX_BUTTONS (0) - -/** - * @brief Touch controller type - * - */ -typedef struct esp_lcd_touch_s esp_lcd_touch_t; -typedef esp_lcd_touch_t *esp_lcd_touch_handle_t; - -/** - * @brief Touch controller interrupt callback type - * - */ -typedef void (*esp_lcd_touch_interrupt_callback_t)(esp_lcd_touch_handle_t tp); - -/** - * @brief Touch Configuration Type - * - */ -typedef struct { - uint16_t x_max; /*!< X coordinates max (for mirroring) */ - uint16_t y_max; /*!< Y coordinates max (for mirroring) */ - - gpio_num_t rst_gpio_num; /*!< GPIO number of reset pin */ - gpio_num_t int_gpio_num; /*!< GPIO number of interrupt pin */ - - struct { - unsigned int reset: 1; /*!< Level of reset pin in reset */ - unsigned int interrupt: 1;/*!< Active Level of interrupt pin */ - } levels; - - struct { - unsigned int swap_xy: 1; /*!< Swap X and Y after read coordinates */ - unsigned int mirror_x: 1; /*!< Mirror X after read coordinates */ - unsigned int mirror_y: 1; /*!< Mirror Y after read coordinates */ - } flags; - - /*!< User callback called after get coordinates from touch controller for apply user adjusting */ - void (*process_coordinates)(esp_lcd_touch_handle_t tp, uint16_t *x, uint16_t *y, uint16_t *strength, uint8_t *point_num, uint8_t max_point_num); - /*!< User callback called after the touch interrupt occurred */ - esp_lcd_touch_interrupt_callback_t interrupt_callback; - /*!< User data passed to callback */ - void *user_data; - /*!< User data passed to driver */ - void *driver_data; -} esp_lcd_touch_config_t; - -typedef struct { - uint8_t points; /*!< Count of touch points saved */ - - struct { - uint16_t x; /*!< X coordinate */ - uint16_t y; /*!< Y coordinate */ - uint16_t strength; /*!< Strength */ - } coords[CONFIG_ESP_LCD_TOUCH_MAX_POINTS]; - -#if (CONFIG_ESP_LCD_TOUCH_MAX_BUTTONS > 0) - uint8_t buttons; /*!< Count of buttons states saved */ - - struct { - uint8_t status; /*!< Status of button */ - } button[CONFIG_ESP_LCD_TOUCH_MAX_BUTTONS]; -#endif - - portMUX_TYPE lock; /*!< Lock for read/write */ -} esp_lcd_touch_data_t; - -/** - * @brief Declare of Touch Type - * - */ -struct esp_lcd_touch_s { - - /** - * @brief set touch controller into sleep mode - * - * @note This function is usually blocking. - * - * @param tp: Touch handler - * - * @return - * - ESP_OK on success, otherwise returns ESP_ERR_xxx - */ - esp_err_t (*enter_sleep)(esp_lcd_touch_handle_t tp); - - /** - * @brief set touch controller into normal mode - * - * @note This function is usually blocking. - * - * @param tp: Touch handler - * - * @return - * - ESP_OK on success, otherwise returns ESP_ERR_xxx - */ - esp_err_t (*exit_sleep)(esp_lcd_touch_handle_t tp); - - /** - * @brief Read data from touch controller (mandatory) - * - * @note This function is usually blocking. - * - * @param tp: Touch handler - * - * @return - * - ESP_OK on success, otherwise returns ESP_ERR_xxx - */ - esp_err_t (*read_data)(esp_lcd_touch_handle_t tp); - - /** - * @brief Get coordinates from touch controller (mandatory) - * - * @param tp: Touch handler - * @param x: Array of X coordinates - * @param y: Array of Y coordinates - * @param strength: Array of strengths - * @param point_num: Count of points touched (equals with count of items in x and y array) - * @param max_point_num: Maximum count of touched points to return (equals with max size of x and y array) - * - * @return - * - Returns true, when touched and coordinates readed. Otherwise returns false. - */ - bool (*get_xy)(esp_lcd_touch_handle_t tp, uint16_t *x, uint16_t *y, uint16_t *strength, uint8_t *point_num, uint8_t max_point_num); - - -#if (CONFIG_ESP_LCD_TOUCH_MAX_BUTTONS > 0) - /** - * @brief Get button state (optional) - * - * @param tp: Touch handler - * @param n: Button index - * @param state: Button state - * - * @return - * - Returns true, when touched and coordinates readed. Otherwise returns false. - */ - esp_err_t (*get_button_state)(esp_lcd_touch_handle_t tp, uint8_t n, uint8_t *state); -#endif - - /** - * @brief Swap X and Y after read coordinates (optional) - * If set, then not used SW swapping. - * - * @param tp: Touch handler - * @param swap: Set swap value - * - * @return - * - ESP_OK on success, otherwise returns ESP_ERR_xxx - */ - esp_err_t (*set_swap_xy)(esp_lcd_touch_handle_t tp, bool swap); - - /** - * @brief Are X and Y coordinates swapped (optional) - * - * @param tp: Touch handler - * @param swap: Get swap value - * - * @return - * - ESP_OK on success, otherwise returns ESP_ERR_xxx - */ - esp_err_t (*get_swap_xy)(esp_lcd_touch_handle_t tp, bool *swap); - - /** - * @brief Mirror X after read coordinates - * If set, then not used SW mirroring. - * - * @param tp: Touch handler - * @param mirror: Set X mirror value - * - * @return - * - ESP_OK on success, otherwise returns ESP_ERR_xxx - */ - esp_err_t (*set_mirror_x)(esp_lcd_touch_handle_t tp, bool mirror); - - /** - * @brief Is mirrored X (optional) - * - * @param tp: Touch handler - * @param mirror: Get X mirror value - * - * @return - * - ESP_OK on success, otherwise returns ESP_ERR_xxx - */ - esp_err_t (*get_mirror_x)(esp_lcd_touch_handle_t tp, bool *mirror); - - /** - * @brief Mirror Y after read coordinates - * If set, then not used SW mirroring. - * - * @param tp: Touch handler - * @param mirror: Set Y mirror value - * - * @return - * - ESP_OK on success, otherwise returns ESP_ERR_xxx - */ - esp_err_t (*set_mirror_y)(esp_lcd_touch_handle_t tp, bool mirror); - - /** - * @brief Is mirrored Y (optional) - * - * @param tp: Touch handler - * @param mirror: Get Y mirror value - * - * @return - * - ESP_OK on success, otherwise returns ESP_ERR_xxx - */ - esp_err_t (*get_mirror_y)(esp_lcd_touch_handle_t tp, bool *mirror); - - /** - * @brief Delete Touch - * - * @param tp: Touch handler - * - * @return - * - ESP_OK on success, otherwise returns ESP_ERR_xxx - */ - esp_err_t (*del)(esp_lcd_touch_handle_t tp); - - /** - * @brief Configuration structure - */ - esp_lcd_touch_config_t config; - - /** - * @brief Communication interface - */ - esp_lcd_panel_io_handle_t io; - - /** - * @brief Data structure - */ - esp_lcd_touch_data_t data; -}; - -/** - * @brief Read data from touch controller - * - * @note This function is usually blocking. - * - * @param tp: Touch handler - * - * @return - * - ESP_OK on success - * - ESP_ERR_INVALID_ARG parameter error - * - ESP_FAIL sending command error, slave hasn't ACK the transfer - * - ESP_ERR_INVALID_STATE I2C driver not installed or not in master mode - * - ESP_ERR_TIMEOUT operation timeout because the bus is busy - */ -esp_err_t esp_lcd_touch_read_data(esp_lcd_touch_handle_t tp); - -/** - * @brief Read coordinates from touch controller - * - * @param tp: Touch handler - * @param x: Array of X coordinates - * @param y: Array of Y coordinates - * @param strength: Array of the strengths (can be NULL) - * @param point_num: Count of points touched (equals with count of items in x and y array) - * @param max_point_num: Maximum count of touched points to return (equals with max size of x and y array) - * - * @return - * - Returns true, when touched and coordinates readed. Otherwise returns false. - */ -bool esp_lcd_touch_get_coordinates(esp_lcd_touch_handle_t tp, uint16_t *x, uint16_t *y, uint16_t *strength, uint8_t *point_num, uint8_t max_point_num); - - -#if (CONFIG_ESP_LCD_TOUCH_MAX_BUTTONS > 0) -/** - * @brief Get button state - * - * @param tp: Touch handler - * @param n: Button index - * @param state: Button state - * - * @return - * - ESP_OK on success - * - ESP_ERR_NOT_SUPPORTED if this function is not supported by controller - * - ESP_ERR_INVALID_ARG if bad button index - */ -esp_err_t esp_lcd_touch_get_button_state(esp_lcd_touch_handle_t tp, uint8_t n, uint8_t *state); -#endif - -/** - * @brief Swap X and Y after read coordinates - * - * @param tp: Touch handler - * @param swap: Set swap value - * - * @return - * - ESP_OK on success - */ -esp_err_t esp_lcd_touch_set_swap_xy(esp_lcd_touch_handle_t tp, bool swap); - -/** - * @brief Are X and Y coordinates swapped - * - * @param tp: Touch handler - * @param swap: Get swap value - * - * @return - * - ESP_OK on success - */ -esp_err_t esp_lcd_touch_get_swap_xy(esp_lcd_touch_handle_t tp, bool *swap); - -/** - * @brief Mirror X after read coordinates - * - * @param tp: Touch handler - * @param mirror: Set X mirror value - * - * @return - * - ESP_OK on success - */ -esp_err_t esp_lcd_touch_set_mirror_x(esp_lcd_touch_handle_t tp, bool mirror); - -/** - * @brief Is mirrored X - * - * @param tp: Touch handler - * @param mirror: Get X mirror value - * - * @return - * - ESP_OK on success - */ -esp_err_t esp_lcd_touch_get_mirror_x(esp_lcd_touch_handle_t tp, bool *mirror); - -/** - * @brief Mirror Y after read coordinates - * - * @param tp: Touch handler - * @param mirror: Set Y mirror value - * - * @return - * - ESP_OK on success - */ -esp_err_t esp_lcd_touch_set_mirror_y(esp_lcd_touch_handle_t tp, bool mirror); - -/** - * @brief Is mirrored Y - * - * @param tp: Touch handler - * @param mirror: Get Y mirror value - * - * @return - * - ESP_OK on success - */ -esp_err_t esp_lcd_touch_get_mirror_y(esp_lcd_touch_handle_t tp, bool *mirror); - -/** - * @brief Delete touch (free all allocated memory and restart HW) - * - * @param tp: Touch handler - * - * @return - * - ESP_OK on success - */ -esp_err_t esp_lcd_touch_del(esp_lcd_touch_handle_t tp); - -/** - * @brief Register user callback called after the touch interrupt occurred - * - * @param tp: Touch handler - * @param callback: Interrupt callback - * - * @return - * - ESP_OK on success - */ -esp_err_t esp_lcd_touch_register_interrupt_callback(esp_lcd_touch_handle_t tp, esp_lcd_touch_interrupt_callback_t callback); - -/** - * @brief Register user callback called after the touch interrupt occurred with user data - * - * @param tp: Touch handler - * @param callback: Interrupt callback - * @param user_data: User data passed to callback - * - * @return - * - ESP_OK on success - */ -esp_err_t esp_lcd_touch_register_interrupt_callback_with_data(esp_lcd_touch_handle_t tp, esp_lcd_touch_interrupt_callback_t callback, void *user_data); - -/** - * @brief Enter sleep mode - * - * @param tp: Touch handler - * - * @return - * - ESP_OK on success - * - ESP_ERR_INVALID_ARG if parameter is invalid - */ -esp_err_t esp_lcd_touch_enter_sleep(esp_lcd_touch_handle_t tp); - -/** - * @brief Exit sleep mode - * - * @param tp: Touch handler - * - * @return - * - ESP_OK on success - * - ESP_ERR_INVALID_ARG if parameter is invalid - */ -esp_err_t esp_lcd_touch_exit_sleep(esp_lcd_touch_handle_t tp); - -#ifdef __cplusplus -} -#endif diff --git a/src/vendor/my_panel_io.c b/src/vendor/my_panel_io.c deleted file mode 100644 index 3c33516..0000000 --- a/src/vendor/my_panel_io.c +++ /dev/null @@ -1,160 +0,0 @@ -#ifdef BOARD_IS_JC3248W535_VENDOR -// Custom esp_lcd_panel_io_t implementation for QSPI on arduino-esp32 3.0.x. -// -// Replicates stock `esp_lcd_panel_io_spi` behavior for lcd_cmd_bits=32 + -// flags.quad_mode=true, which arduino-esp32 3.0.x doesn't ship. Behavior -// verified against ESP-IDF v5.2 src/esp_lcd_panel_io_spi.c: -// -// * Cmd is a SEPARATE 32-bit QIO data transaction. Not packed with params. -// Not split across cmd/addr phases. -// * Params (for tx_param) and color data (for tx_color) follow as additional -// data-phase-only QIO transactions. -// * CS is held LOW across all transactions of one logical operation via -// SPI_TRANS_CS_KEEP_ACTIVE — this requires the SPI peripheral to drive CS -// (spics_io_num must be set to the CS pin, NOT -1). -// * No explicit RAMWRCONT header on continuation chunks. The chip stays in -// write mode as long as CS stays asserted. -// -// Scope: display path only. - -#include -#include -#include "esp_log.h" -#include "esp_check.h" -#include "driver/spi_master.h" -#include "esp_lcd_panel_io.h" -#include "esp_lcd_panel_io_interface.h" -#include "esp_heap_caps.h" - -#include "my_panel_io.h" - -static const char *TAG = "my_panel_io"; - -#define SCRATCH_BYTES 4096 -#define HEADER_BYTES 4 - -typedef struct { - esp_lcd_panel_io_t base; - spi_device_handle_t spi; - uint8_t* scratch; // DMA-capable staging for PSRAM-sourced data - uint8_t cmdbuf[8]; // 32-bit packed cmd buffer (DMA-safe internal) -} my_io_t; - -static inline void pack_header(uint8_t* buf, int lcd_cmd) { - buf[0] = (lcd_cmd >> 24) & 0xFF; - buf[1] = (lcd_cmd >> 16) & 0xFF; - buf[2] = (lcd_cmd >> 8) & 0xFF; - buf[3] = (lcd_cmd ) & 0xFF; -} - -// Send the 32-bit packed lcd_cmd as its own QIO data-phase transaction. -// CS is kept active so subsequent transactions in the same op continue the -// transfer under one CS assertion. -static esp_err_t send_cmd(my_io_t* self, int lcd_cmd, bool more_to_follow) { - pack_header(self->cmdbuf, lcd_cmd); - spi_transaction_ext_t t = {}; - t.base.flags = SPI_TRANS_MODE_QIO - | SPI_TRANS_VARIABLE_CMD - | SPI_TRANS_VARIABLE_ADDR - | SPI_TRANS_VARIABLE_DUMMY - | (more_to_follow ? SPI_TRANS_CS_KEEP_ACTIVE : 0); - t.command_bits = 0; - t.address_bits = 0; - t.dummy_bits = 0; - t.base.tx_buffer = self->cmdbuf; - t.base.length = 32; // 4 bytes = 32 bits - return spi_device_polling_transmit(self->spi, (spi_transaction_t*)&t); -} - -// Send a data-phase-only QIO transaction. Caller controls CS via the -// cs_keep_active flag: true = keep asserted after this one, false = release. -// tx_buffer must be in DMA-capable memory. -static esp_err_t send_data(my_io_t* self, const uint8_t* buf, size_t nbytes, - bool cs_keep_active) { - spi_transaction_ext_t t = {}; - t.base.flags = SPI_TRANS_MODE_QIO - | SPI_TRANS_VARIABLE_CMD - | SPI_TRANS_VARIABLE_ADDR - | SPI_TRANS_VARIABLE_DUMMY - | (cs_keep_active ? SPI_TRANS_CS_KEEP_ACTIVE : 0); - t.command_bits = 0; - t.address_bits = 0; - t.dummy_bits = 0; - t.base.tx_buffer = buf; - t.base.length = nbytes * 8; - return spi_device_polling_transmit(self->spi, (spi_transaction_t*)&t); -} - -static esp_err_t my_tx_param(esp_lcd_panel_io_t* io, int lcd_cmd, - const void* param, size_t param_size) -{ - my_io_t* self = (my_io_t*)io; - bool has_param = (param && param_size > 0); - esp_err_t r = send_cmd(self, lcd_cmd, /*more_to_follow=*/has_param); - if (r != ESP_OK || !has_param) return r; - - // Copy param into DMA scratch (param is usually a stack-allocated - // compound literal from the vendor driver, not DMA-capable). - if (param_size > SCRATCH_BYTES) return ESP_ERR_INVALID_SIZE; - memcpy(self->scratch, param, param_size); - return send_data(self, self->scratch, param_size, /*cs_keep_active=*/false); -} - -static esp_err_t my_tx_color(esp_lcd_panel_io_t* io, int lcd_cmd, - const void* color, size_t color_size) -{ - my_io_t* self = (my_io_t*)io; - esp_err_t r = ESP_OK; - // Per the jc3248w535 skill: AXS15231B wants CS toggled per chunk with a - // RAMWR/RAMWRCONT header on each chunk. lcd_cmd from the vendor driver - // is already (0x32<<24)|(0x2C<<8) (or 0x3C for continuation). We emit - // that header on the first chunk and (0x32<<24)|(0x3C<<8) on all - // continuations. - const int cont_lcd_cmd = (0x32 << 24) | (0x3C << 8); - const uint8_t* src = (const uint8_t*)color; - size_t remaining = color_size; - bool first = true; - while (remaining) { - size_t data_capacity = SCRATCH_BYTES - HEADER_BYTES; - size_t chunk = remaining > data_capacity ? data_capacity : remaining; - - pack_header(self->scratch, first ? lcd_cmd : cont_lcd_cmd); - memcpy(self->scratch + HEADER_BYTES, src, chunk); - - // CS cycles per chunk (spics_io_num is set, peripheral does the toggle). - // No SPI_TRANS_CS_KEEP_ACTIVE. - r = send_data(self, self->scratch, HEADER_BYTES + chunk, - /*cs_keep_active=*/false); - if (r != ESP_OK) break; - first = false; - src += chunk; - remaining -= chunk; - } - return r; -} - -static esp_err_t my_del(esp_lcd_panel_io_t* io) { - my_io_t* self = (my_io_t*)io; - if (self->scratch) heap_caps_free(self->scratch); - free(self); - return ESP_OK; -} - -esp_err_t my_panel_io_new(spi_device_handle_t spi, int cs_pin, - esp_lcd_panel_io_handle_t *ret_io) -{ - (void)cs_pin; // CS is managed by the SPI peripheral via spics_io_num - if (!spi || !ret_io) return ESP_ERR_INVALID_ARG; - my_io_t* self = (my_io_t*)calloc(1, sizeof(my_io_t)); - if (!self) return ESP_ERR_NO_MEM; - self->spi = spi; - self->scratch = (uint8_t*)heap_caps_aligned_alloc(16, SCRATCH_BYTES, - MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL); - if (!self->scratch) { free(self); return ESP_ERR_NO_MEM; } - self->base.tx_param = my_tx_param; - self->base.tx_color = my_tx_color; - self->base.del = my_del; - *ret_io = &self->base; - return ESP_OK; -} -#endif // BOARD_IS_JC3248W535_VENDOR diff --git a/src/vendor/my_panel_io.h b/src/vendor/my_panel_io.h deleted file mode 100644 index f57565a..0000000 --- a/src/vendor/my_panel_io.h +++ /dev/null @@ -1,25 +0,0 @@ -#pragma once - -#include "esp_err.h" -#include "driver/spi_master.h" -#include "esp_lcd_panel_io.h" - -#ifdef __cplusplus -extern "C" { -#endif - -// Create a custom esp_lcd_panel_io_t that sends commands + params over QSPI -// using a single data-phase transaction (no cmd/addr phase split). -// -// The SPI device must already be added to the bus. The caller retains -// ownership of the spi handle; my_del does not free it. -// -// cs_pin is the GPIO pin for CS. This implementation drives CS manually -// around each transaction so the SPI device must have been configured with -// spics_io_num = -1. -esp_err_t my_panel_io_new(spi_device_handle_t spi, int cs_pin, - esp_lcd_panel_io_handle_t *ret_io); - -#ifdef __cplusplus -} -#endif