upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/DISPLAY_FIX_PLAN.md
diff options
context:
space:
mode:
authorYour Name <you@example.com>2026-05-20 02:14:45 +0530
committerYour Name <you@example.com>2026-05-20 02:14:45 +0530
commit899016795c389151e6b486ec470653f5688e5c5f (patch)
tree64b4c732d6f585552fd14df5f47890764c66ac1a /DISPLAY_FIX_PLAN.md
parent4f713120dbedd37a3512c2ec1af0766025a59d57 (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.md174
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
12Register 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
28The 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
30ArduinoGFX 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
32Our 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
42ArduinoGFX 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
47Our 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
49A 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
60All 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
107At init, allocate a static buffer:
108```c
109#define FLUSH_CHUNK_PIXELS 2048 // 4096 bytes, fits in internal DMA RAM
110static uint8_t *s_swap_buf = NULL;
111
112// In axs15231b_init():
113s_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
119void 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
160Only re-render when:
161- `s_force_render` is set (state change, init)
162- QR mode cycles (every 5s in READY state)
163
164This 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 |