diff options
| author | Your Name <you@example.com> | 2026-05-20 02:14:45 +0530 |
|---|---|---|
| committer | Your Name <you@example.com> | 2026-05-20 02:14:45 +0530 |
| commit | 899016795c389151e6b486ec470653f5688e5c5f (patch) | |
| tree | 64b4c732d6f585552fd14df5f47890764c66ac1a /DISPLAY_FIX_PLAN.md | |
| parent | 4f713120dbedd37a3512c2ec1af0766025a59d57 (diff) | |
feat: merge display-fix — touch driver, keyboard, WiFi setup UI
Squash-merge of feature/display-fix (27 commits):
- AXS15231B touch driver with coordinate parsing (touch.c/h)
- On-screen keyboard with layout/hit detection (keyboard.c/h)
- WiFi setup state machine + config_add_wifi (wifi_setup.c/h)
- Web-based WiFi setup via captive portal
- Display rotation fix (stride=480), color fixes, DMA byte-swap
- WiFi QR code on BOOT and ERROR screens
- ERROR state transition after WiFi retries exhausted
- Unit tests: test_touch, test_keyboard, test_wifi_setup
- Integration test: wifi_setup.mjs
- E2E test: wifi-setup.spec.mjs
- Per-board hardware locks for flash targets
Conflicts resolved: took master for config/cvm/api/main (more current),
took display-fix for display/axs15231b (newer fixes).
Diffstat (limited to 'DISPLAY_FIX_PLAN.md')
| -rw-r--r-- | DISPLAY_FIX_PLAN.md | 174 |
1 files changed, 174 insertions, 0 deletions
diff --git a/DISPLAY_FIX_PLAN.md b/DISPLAY_FIX_PLAN.md new file mode 100644 index 0000000..7fa4882 --- /dev/null +++ b/DISPLAY_FIX_PLAN.md | |||
| @@ -0,0 +1,174 @@ | |||
| 1 | # Display Fix Plan — AXS15231B QSPI Driver | ||
| 2 | |||
| 3 | ## Board Info | ||
| 4 | |||
| 5 | - **Board:** Guition JC3248W535C_I_Y (Board C, `/dev/ttyACM0`) | ||
| 6 | - **Display IC:** AXS15231B (QSPI, 4 data lines) | ||
| 7 | - **Resolution:** 320x480 portrait (native), 480x320 landscape (via rotation) | ||
| 8 | - **Pins:** CS=45, CLK=47, D0=21, D1=48, D2=40, D3=39, BL=1 | ||
| 9 | |||
| 10 | ## QSPI Protocol (from ArduinoGFX source) | ||
| 11 | |||
| 12 | Register writes and pixel data use different QSPI framing: | ||
| 13 | |||
| 14 | | Operation | cmd | addr | flags | Data format | | ||
| 15 | |-----------|-----|------|-------|-------------| | ||
| 16 | | Register write (C8D8) | `0x02` | `LCD_CMD << 8` | `MULTILINE_CMD \| MULTILINE_ADDR` | Big-endian | | ||
| 17 | | Register write (C8D16) | `0x02` | `LCD_CMD << 8` | `MULTILINE_CMD \| MULTILINE_ADDR` | Big-endian | | ||
| 18 | | Register write (C8D16D16) | `0x02` | `LCD_CMD << 8` | `MULTILINE_CMD \| MULTILINE_ADDR` | Big-endian | | ||
| 19 | | Pixel data (first chunk) | `0x32` | `0x003C00` | `SPI_TRANS_MODE_QIO` | Big-endian (byte-swapped) | | ||
| 20 | | Pixel data (continuation) | — | — | `MODE_QIO \| VAR_CMD \| VAR_ADDR \| VAR_DUMMY` | Big-endian (byte-swapped) | | ||
| 21 | |||
| 22 | - CS: Manual GPIO control (`spics_io_num = -1`) | ||
| 23 | - Bus: permanently acquired via `spi_device_acquire_bus()` | ||
| 24 | - SPI config: `command_bits=8, address_bits=24, dummy_bits=0, mode=0, HALFDUPLEX` | ||
| 25 | |||
| 26 | ## Root Cause: Byte-Order Mismatch | ||
| 27 | |||
| 28 | The ESP32-S3 is little-endian. The framebuffer stores RGB565 pixels as `[low_byte, high_byte]`. The AXS15231B expects pixels in big-endian order `[high_byte, low_byte]` over QSPI. | ||
| 29 | |||
| 30 | ArduinoGFX handles this by byte-swapping each pixel in `writePixels()` and `writeRepeat()` using the `MSB_16_SET(var, val)` macro: `var = (val >> 8) | (val << 8)`. | ||
| 31 | |||
| 32 | Our driver was sending raw little-endian pixels, causing the display to interpret the byte-swapped values as colors. Example: | ||
| 33 | |||
| 34 | | Intended color | RGB565 hex | Display sees (no swap) | Display sees (with swap) | | ||
| 35 | |---------------|-----------|----------------------|------------------------| | ||
| 36 | | Pink 0xF79F | `[9F, F7]` | R=19, G=63, B=23 (green) | R=30, G=60, B=31 (pink/white) | | ||
| 37 | | Red 0xF800 | `[00, F8]` | R=0, G=0, B=0 (black!) | R=31, G=0, B=0 (red) | | ||
| 38 | | Cyan 0x07FF | `[FF, 07]` | R=31, G=63, B=7 (yellow) | R=0, G=63, B=31 (cyan) | | ||
| 39 | |||
| 40 | ## Root Cause: PSRAM Cache Coherency | ||
| 41 | |||
| 42 | ArduinoGFX allocates its pixel transfer buffer in **internal DMA SRAM**: | ||
| 43 | ```cpp | ||
| 44 | _buffer = (uint8_t *)heap_caps_aligned_alloc(16, ESP32QSPI_MAX_PIXELS_AT_ONCE * 2, MALLOC_CAP_DMA); | ||
| 45 | ``` | ||
| 46 | |||
| 47 | Our framebuffer lives in PSRAM (8MB). When we modified the PSRAM framebuffer in-place (byte-swap), the CPU cache held the modified values but the SPI DMA controller read stale data from physical PSRAM. Result: black screen. | ||
| 48 | |||
| 49 | A separate allocation (even in PSRAM) works because it gets clean, freshly-written cache lines. | ||
| 50 | |||
| 51 | ## Reference Implementations Studied | ||
| 52 | |||
| 53 | | Repo | Chip | Bus | Notes | | ||
| 54 | |------|------|-----|-------| | ||
| 55 | | [me-processware/JC3248W535-Driver](https://github.com/me-processware/JC3248W535-Driver) | AXS15231B | Arduino_ESP32QSPI | Arduino_Canvas wrapper, same pins | | ||
| 56 | | [F1ATB/JC3248W535-Demo](https://github.com/F1ATB/JC3248W535-Demo) | AXS15231B | Arduino_ESP32QSPI | Minimal demo, rotation=1 landscape | | ||
| 57 | | [AudunKodehode/JC3248W535EN-Touch-LCD](https://github.com/AudunKodehode/JC3248W535EN-Touch-LCD) | AXS15231B | Arduino_ESP32QSPI | Full library, QR codes, JPEG, coordinate transforms | | ||
| 58 | | [ArduinoGFX Arduino_ESP32QSPI.cpp](https://github.com/moononournation/Arduino_GFX) | — | — | Reference QSPI protocol implementation | | ||
| 59 | |||
| 60 | All use identical pin assignments and bus configuration. | ||
| 61 | |||
| 62 | ## Checklist | ||
| 63 | |||
| 64 | ### Done | ||
| 65 | - [x] Created worktree on branch `feature/display-fix` | ||
| 66 | - [x] Tracked untracked display files into branch | ||
| 67 | - [x] Added Board C support to Makefile (`flash-c`, `lock-c`, etc.) | ||
| 68 | - [x] Diagnosed root cause: QSPI protocol, not standard SPI | ||
| 69 | - [x] Fetched and analyzed ArduinoGFX QSPI source code | ||
| 70 | - [x] Discovered correct QSPI framing: `cmd=0x02` for regs, `cmd=0x32/addr=0x003C00` for pixels | ||
| 71 | - [x] Rewrote driver with correct QSPI protocol | ||
| 72 | - [x] Build succeeds, flash succeeds | ||
| 73 | - [x] Display shows recognizable text ("TollGate", "starting") — protocol confirmed working | ||
| 74 | - [x] Identified byte-swap requirement (green text = wrong byte order) | ||
| 75 | - [x] Identified PSRAM cache coherency issue (in-place swap = black screen) | ||
| 76 | - [x] Studied 3 reference implementations + ArduinoGFX source | ||
| 77 | - [x] Text positions adjusted for 320x480 portrait centering | ||
| 78 | - [x] Internal DMA byte-swap buffer (MALLOC_CAP_DMA, 4KB chunks) | ||
| 79 | - [x] **CRITICAL FIX: Added RAMWR (0x2C) before pixel data** — fixed wrapping/double-vision | ||
| 80 | - [x] Display shows correct colors: cyan TollGate + yellow starting... centered on black | ||
| 81 | - [x] Reduced font scale to 2/1 for clean readability | ||
| 82 | - [x] Implemented full UI: BOOT, READY (QR cycling), PAYMENT, ERROR screens | ||
| 83 | - [x] WiFi events trigger display state transitions (READY ↔ ERROR) | ||
| 84 | - [x] Color-coded wallet balance (green/yellow/red) | ||
| 85 | - [x] **ALL SCREENS VERIFIED WORKING ON HARDWARE** | ||
| 86 | |||
| 87 | ### In Progress | ||
| 88 | - [ ] (nothing) | ||
| 89 | |||
| 90 | ### TODO | ||
| 91 | - [ ] Run `make test-unit` to check for regressions | ||
| 92 | - [ ] Commit, push, and prepare for merge to master | ||
| 93 | - [ ] Restore render-on-change logic (proven correct, black screen was from swap not logic) | ||
| 94 | - [ ] Use saturated colors: cyan `0x07FF`, yellow `0xFFE0`, white `0xFFFF` | ||
| 95 | - [ ] Build, flash, verify correct colors and stable text | ||
| 96 | - [ ] Verify QR code rendering in READY state | ||
| 97 | - [ ] Verify payment/error screen states | ||
| 98 | - [ ] Remove debug log from flush | ||
| 99 | - [ ] Run `make test-unit` to check for regressions | ||
| 100 | - [ ] Commit working display driver | ||
| 101 | - [ ] Push to remote | ||
| 102 | |||
| 103 | ## Implementation Plan | ||
| 104 | |||
| 105 | ### 1. Internal DMA swap buffer in `axs15231b.c` | ||
| 106 | |||
| 107 | At init, allocate a static buffer: | ||
| 108 | ```c | ||
| 109 | #define FLUSH_CHUNK_PIXELS 2048 // 4096 bytes, fits in internal DMA RAM | ||
| 110 | static uint8_t *s_swap_buf = NULL; | ||
| 111 | |||
| 112 | // In axs15231b_init(): | ||
| 113 | s_swap_buf = heap_caps_aligned_alloc(16, FLUSH_CHUNK_PIXELS * 2, MALLOC_CAP_DMA); | ||
| 114 | ``` | ||
| 115 | |||
| 116 | ### 2. Byte-swap flush loop | ||
| 117 | |||
| 118 | ```c | ||
| 119 | void axs15231b_flush(void) { | ||
| 120 | // ... CASET, RASET ... | ||
| 121 | |||
| 122 | int total_pixels = s_width * s_height; | ||
| 123 | int offset = 0; | ||
| 124 | bool first = true; | ||
| 125 | |||
| 126 | cs_low(); | ||
| 127 | while (offset < total_pixels) { | ||
| 128 | int chunk = min(FLUSH_CHUNK_PIXELS, total_pixels - offset); | ||
| 129 | |||
| 130 | // Byte-swap from PSRAM framebuffer into DMA buffer | ||
| 131 | uint8_t *src = (uint8_t *)(s_fb + offset); | ||
| 132 | for (int i = 0; i < chunk * 2; i += 2) { | ||
| 133 | s_swap_buf[i] = src[i + 1]; | ||
| 134 | s_swap_buf[i + 1] = src[i]; | ||
| 135 | } | ||
| 136 | |||
| 137 | // Send via QSPI | ||
| 138 | spi_transaction_ext_t t = {0}; | ||
| 139 | if (first) { | ||
| 140 | t.base.flags = SPI_TRANS_MODE_QIO; | ||
| 141 | t.base.cmd = 0x32; | ||
| 142 | t.base.addr = 0x003C00; | ||
| 143 | first = false; | ||
| 144 | } else { | ||
| 145 | t.base.flags = SPI_TRANS_MODE_QIO | SPI_TRANS_VARIABLE_CMD | | ||
| 146 | SPI_TRANS_VARIABLE_ADDR | SPI_TRANS_VARIABLE_DUMMY; | ||
| 147 | } | ||
| 148 | t.base.tx_buffer = s_swap_buf; | ||
| 149 | t.base.length = chunk * 16; | ||
| 150 | spi_device_polling_transmit(s_spi, (spi_transaction_t *)&t); | ||
| 151 | |||
| 152 | offset += chunk; | ||
| 153 | } | ||
| 154 | cs_high(); | ||
| 155 | } | ||
| 156 | ``` | ||
| 157 | |||
| 158 | ### 3. Render-on-change in `display.c` | ||
| 159 | |||
| 160 | Only re-render when: | ||
| 161 | - `s_force_render` is set (state change, init) | ||
| 162 | - QR mode cycles (every 5s in READY state) | ||
| 163 | |||
| 164 | This eliminates the 1Hz full-screen redraw that caused text to "move around." | ||
| 165 | |||
| 166 | ### 4. Color choices | ||
| 167 | |||
| 168 | | Element | Old color | New color | Reason | | ||
| 169 | |---------|-----------|-----------|--------| | ||
| 170 | | Boot title | `0xF79F` (near-white) | `0x07FF` (cyan) | High contrast on black | | ||
| 171 | | Boot subtitle | `0xB5B6` (gray) | `0xFFE0` (yellow) | Visible, warm accent | | ||
| 172 | | Ready label | `0xB5B6` | `0x07FF` | Consistent accent | | ||
| 173 | | Payment bg | `0x07E0` (green) | `0x07E0` | Keep — bright green is clear | | ||
| 174 | | Error bg | `0xF800` (red) | `0xF800` | Keep — bright red is clear | | ||