Experimental JC3248W535 (Guition 3.5" AXS15231B) support#74
Draft
theNailz wants to merge 15 commits intoKeralots:mainfrom
Draft
Experimental JC3248W535 (Guition 3.5" AXS15231B) support#74theNailz wants to merge 15 commits intoKeralots:mainfrom
theNailz wants to merge 15 commits intoKeralots:mainfrom
Conversation
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.
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.
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.
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.
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.
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.
…wrapper 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.
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.
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.
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.
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.
…is set 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.
…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.
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.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds an experimental build target for the Guition JC3248W535 — a 3.5" 320x480 IPS ESP32-S3 board with an AXS15231B QSPI in-cell display + capacitive touch.
jc3248w535inplatformio.ini, new layoutinclude/layout_320x480.hsrc/lgfx_panel_axs15231b_agfx.hppwraps Arduino_GFX's AXS15231B inside a LovyanGFXPanel_Device(LovyanGFX mainline has no Panel_AXS15231B and no Bus_QSPI)pushRawPixels()— the chip cannot address arbitrary Y per draw in QSPI modeWhat works
What's deliberately out of scope (follow-ups)
LY_LAND_*layout variant exists for 320x480 yet; web-UI selection of 1/3 snaps to 0 with a Serial warning rather than rendering a broken layoutflushFrame()— currently pushes every loop tick unconditionally; fine on USB power, suboptimal for batteryArchitecture notes
Three chip-level invariants the driver preserves, discovered on-device:
Panel_Device::writePixelschunks a frame into many CS cycles, which produces full image destruction with repeating horizontal bands. The wrapper exposespushRawPixels(buf, n)as an escape hatch the UI layer calls directly.Existing board targets (
esp32s3,cyd, etc.) are gated byBOARD_IS_*build flags and are unaffected.Test plan
jc3248w535clean, flash to COM12 (ESP32-S3-WROOM-1, 16 MB flash, 8 MB PSRAM)esp32s3andcydenvs (compile clean, no runtime change)